github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "errors" 9 "fmt" 10 "io/ioutil" 11 "net/http" 12 "path" 13 "reflect" 14 "time" 15 16 "github.com/Azure/azure-sdk-for-go/arm/compute" 17 "github.com/Azure/azure-sdk-for-go/arm/network" 18 "github.com/Azure/azure-sdk-for-go/arm/resources/resources" 19 "github.com/Azure/azure-sdk-for-go/arm/storage" 20 autorestazure "github.com/Azure/go-autorest/autorest/azure" 21 "github.com/Azure/go-autorest/autorest/mocks" 22 "github.com/Azure/go-autorest/autorest/to" 23 gitjujutesting "github.com/juju/testing" 24 jc "github.com/juju/testing/checkers" 25 "github.com/juju/utils" 26 "github.com/juju/utils/arch" 27 gc "gopkg.in/check.v1" 28 "gopkg.in/juju/names.v2" 29 30 "github.com/juju/juju/api" 31 "github.com/juju/juju/cloudconfig/instancecfg" 32 "github.com/juju/juju/constraints" 33 "github.com/juju/juju/environs" 34 "github.com/juju/juju/environs/imagemetadata" 35 "github.com/juju/juju/environs/simplestreams" 36 "github.com/juju/juju/environs/tags" 37 envtesting "github.com/juju/juju/environs/testing" 38 envtools "github.com/juju/juju/environs/tools" 39 "github.com/juju/juju/instance" 40 "github.com/juju/juju/provider/azure" 41 "github.com/juju/juju/provider/azure/internal/armtemplates" 42 "github.com/juju/juju/provider/azure/internal/azureauth" 43 "github.com/juju/juju/provider/azure/internal/azuretesting" 44 "github.com/juju/juju/testing" 45 "github.com/juju/juju/tools" 46 "github.com/juju/version" 47 ) 48 49 const storageAccountName = "juju400d80004b1d0d06f00d" 50 51 var ( 52 quantalImageReference = compute.ImageReference{ 53 Publisher: to.StringPtr("Canonical"), 54 Offer: to.StringPtr("UbuntuServer"), 55 Sku: to.StringPtr("12.10"), 56 Version: to.StringPtr("latest"), 57 } 58 win2012ImageReference = compute.ImageReference{ 59 Publisher: to.StringPtr("MicrosoftWindowsServer"), 60 Offer: to.StringPtr("WindowsServer"), 61 Sku: to.StringPtr("2012-Datacenter"), 62 Version: to.StringPtr("latest"), 63 } 64 centos7ImageReference = compute.ImageReference{ 65 Publisher: to.StringPtr("OpenLogic"), 66 Offer: to.StringPtr("CentOS"), 67 Sku: to.StringPtr("7.1"), 68 Version: to.StringPtr("latest"), 69 } 70 71 sshPublicKeys = []compute.SSHPublicKey{{ 72 Path: to.StringPtr("/home/ubuntu/.ssh/authorized_keys"), 73 KeyData: to.StringPtr(testing.FakeAuthKeys), 74 }} 75 linuxOsProfile = compute.OSProfile{ 76 ComputerName: to.StringPtr("machine-0"), 77 CustomData: to.StringPtr("<juju-goes-here>"), 78 AdminUsername: to.StringPtr("ubuntu"), 79 LinuxConfiguration: &compute.LinuxConfiguration{ 80 DisablePasswordAuthentication: to.BoolPtr(true), 81 SSH: &compute.SSHConfiguration{ 82 PublicKeys: &sshPublicKeys, 83 }, 84 }, 85 } 86 windowsOsProfile = compute.OSProfile{ 87 ComputerName: to.StringPtr("machine-0"), 88 CustomData: to.StringPtr("<juju-goes-here>"), 89 AdminUsername: to.StringPtr("JujuAdministrator"), 90 AdminPassword: to.StringPtr("sorandom"), 91 WindowsConfiguration: &compute.WindowsConfiguration{ 92 ProvisionVMAgent: to.BoolPtr(true), 93 EnableAutomaticUpdates: to.BoolPtr(true), 94 }, 95 } 96 ) 97 98 type environSuite struct { 99 testing.BaseSuite 100 101 provider environs.EnvironProvider 102 requests []*http.Request 103 storageClient azuretesting.MockStorageClient 104 sender azuretesting.Senders 105 retryClock mockClock 106 107 controllerUUID string 108 envTags map[string]*string 109 vmTags map[string]*string 110 group *resources.ResourceGroup 111 vmSizes *compute.VirtualMachineSizeListResult 112 storageAccounts []storage.Account 113 storageAccount *storage.Account 114 storageAccountKeys *storage.AccountListKeysResult 115 ubuntuServerSKUs []compute.VirtualMachineImageResource 116 deployment *resources.Deployment 117 } 118 119 var _ = gc.Suite(&environSuite{}) 120 121 func (s *environSuite) SetUpTest(c *gc.C) { 122 s.BaseSuite.SetUpTest(c) 123 s.storageClient = azuretesting.MockStorageClient{} 124 s.sender = nil 125 s.requests = nil 126 s.retryClock = mockClock{Clock: gitjujutesting.NewClock(time.Time{})} 127 128 s.provider = newProvider(c, azure.ProviderConfig{ 129 Sender: azuretesting.NewSerialSender(&s.sender), 130 RequestInspector: azuretesting.RequestRecorder(&s.requests), 131 NewStorageClient: s.storageClient.NewClient, 132 RetryClock: &gitjujutesting.AutoAdvancingClock{ 133 &s.retryClock, s.retryClock.Advance, 134 }, 135 RandomWindowsAdminPassword: func() string { return "sorandom" }, 136 InteractiveCreateServicePrincipal: azureauth.InteractiveCreateServicePrincipal, 137 }) 138 139 s.controllerUUID = testing.ControllerTag.Id() 140 s.envTags = map[string]*string{ 141 "juju-model-uuid": to.StringPtr(testing.ModelTag.Id()), 142 "juju-controller-uuid": to.StringPtr(s.controllerUUID), 143 } 144 s.vmTags = map[string]*string{ 145 "juju-model-uuid": to.StringPtr(testing.ModelTag.Id()), 146 "juju-controller-uuid": to.StringPtr(s.controllerUUID), 147 "juju-machine-name": to.StringPtr("machine-0"), 148 } 149 150 s.group = &resources.ResourceGroup{ 151 Location: to.StringPtr("westus"), 152 Tags: &s.envTags, 153 Properties: &resources.ResourceGroupProperties{ 154 ProvisioningState: to.StringPtr("Succeeded"), 155 }, 156 } 157 158 vmSizes := []compute.VirtualMachineSize{{ 159 Name: to.StringPtr("Standard_D1"), 160 NumberOfCores: to.Int32Ptr(1), 161 OsDiskSizeInMB: to.Int32Ptr(1047552), 162 ResourceDiskSizeInMB: to.Int32Ptr(51200), 163 MemoryInMB: to.Int32Ptr(3584), 164 MaxDataDiskCount: to.Int32Ptr(2), 165 }} 166 s.vmSizes = &compute.VirtualMachineSizeListResult{Value: &vmSizes} 167 168 s.storageAccount = &storage.Account{ 169 Name: to.StringPtr("my-storage-account"), 170 Type: to.StringPtr("Standard_LRS"), 171 Tags: &s.envTags, 172 Properties: &storage.AccountProperties{ 173 PrimaryEndpoints: &storage.Endpoints{ 174 Blob: to.StringPtr(fmt.Sprintf("https://%s.blob.storage.azurestack.local/", storageAccountName)), 175 }, 176 ProvisioningState: "Succeeded", 177 }, 178 } 179 180 keys := []storage.AccountKey{{ 181 KeyName: to.StringPtr("key-1-name"), 182 Value: to.StringPtr("key-1"), 183 Permissions: storage.FULL, 184 }} 185 s.storageAccountKeys = &storage.AccountListKeysResult{ 186 Keys: &keys, 187 } 188 189 s.ubuntuServerSKUs = []compute.VirtualMachineImageResource{ 190 {Name: to.StringPtr("12.04-LTS")}, 191 {Name: to.StringPtr("12.10")}, 192 {Name: to.StringPtr("14.04-LTS")}, 193 {Name: to.StringPtr("15.04")}, 194 {Name: to.StringPtr("15.10")}, 195 {Name: to.StringPtr("16.04-LTS")}, 196 } 197 198 s.deployment = nil 199 } 200 201 func (s *environSuite) openEnviron(c *gc.C, attrs ...testing.Attrs) environs.Environ { 202 return openEnviron(c, s.provider, &s.sender, attrs...) 203 } 204 205 func openEnviron( 206 c *gc.C, 207 provider environs.EnvironProvider, 208 sender *azuretesting.Senders, 209 attrs ...testing.Attrs, 210 ) environs.Environ { 211 // Opening the environment should not incur network communication, 212 // so we don't set s.sender until after opening. 213 cfg := makeTestModelConfig(c, attrs...) 214 env, err := provider.Open(environs.OpenParams{ 215 Cloud: fakeCloudSpec(), 216 Config: cfg, 217 }) 218 c.Assert(err, jc.ErrorIsNil) 219 220 // Force an explicit refresh of the access token, so it isn't done 221 // implicitly during the tests. 222 *sender = azuretesting.Senders{ 223 discoverAuthSender(), 224 tokenRefreshSender(), 225 } 226 err = azure.ForceTokenRefresh(env) 227 c.Assert(err, jc.ErrorIsNil) 228 return env 229 } 230 231 func prepareForBootstrap( 232 c *gc.C, 233 ctx environs.BootstrapContext, 234 provider environs.EnvironProvider, 235 sender *azuretesting.Senders, 236 attrs ...testing.Attrs, 237 ) environs.Environ { 238 // Opening the environment should not incur network communication, 239 // so we don't set s.sender until after opening. 240 cfg, err := provider.PrepareConfig(environs.PrepareConfigParams{ 241 Config: makeTestModelConfig(c, attrs...), 242 Cloud: fakeCloudSpec(), 243 }) 244 c.Assert(err, jc.ErrorIsNil) 245 246 env, err := provider.Open(environs.OpenParams{ 247 Cloud: fakeCloudSpec(), 248 Config: cfg, 249 }) 250 c.Assert(err, jc.ErrorIsNil) 251 252 *sender = azuretesting.Senders{ 253 discoverAuthSender(), 254 tokenRefreshSender(), 255 } 256 err = env.PrepareForBootstrap(ctx) 257 c.Assert(err, jc.ErrorIsNil) 258 return env 259 } 260 261 func fakeCloudSpec() environs.CloudSpec { 262 return environs.CloudSpec{ 263 Type: "azure", 264 Name: "azure", 265 Region: "westus", 266 Endpoint: "https://api.azurestack.local", 267 IdentityEndpoint: "https://login.microsoftonline.com", 268 StorageEndpoint: "https://storage.azurestack.local", 269 Credential: fakeServicePrincipalCredential(), 270 } 271 } 272 273 func tokenRefreshSender() *azuretesting.MockSender { 274 tokenRefreshSender := azuretesting.NewSenderWithValue(&autorestazure.Token{ 275 AccessToken: "access-token", 276 ExpiresOn: fmt.Sprint(time.Now().Add(time.Hour).Unix()), 277 Type: "Bearer", 278 }) 279 tokenRefreshSender.PathPattern = ".*/oauth2/token" 280 return tokenRefreshSender 281 } 282 283 func discoverAuthSender() *azuretesting.MockSender { 284 const fakeTenantId = "11111111-1111-1111-1111-111111111111" 285 sender := mocks.NewSender() 286 resp := mocks.NewResponseWithStatus("", http.StatusUnauthorized) 287 mocks.SetResponseHeaderValues(resp, "WWW-Authenticate", []string{ 288 fmt.Sprintf( 289 `authorization_uri="https://testing.invalid/%s"`, 290 fakeTenantId, 291 ), 292 }) 293 sender.AppendResponse(resp) 294 return &azuretesting.MockSender{ 295 Sender: sender, 296 PathPattern: ".*/subscriptions/" + fakeSubscriptionId, 297 } 298 } 299 300 func (s *environSuite) initResourceGroupSenders() azuretesting.Senders { 301 resourceGroupName := "juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d" 302 senders := azuretesting.Senders{s.makeSender(".*/resourcegroups/"+resourceGroupName, s.group)} 303 return senders 304 } 305 306 func (s *environSuite) startInstanceSenders(controller bool) azuretesting.Senders { 307 senders := azuretesting.Senders{s.vmSizesSender()} 308 if s.ubuntuServerSKUs != nil { 309 senders = append(senders, s.makeSender(".*/Canonical/.*/UbuntuServer/skus", s.ubuntuServerSKUs)) 310 } 311 senders = append(senders, s.makeSender("/deployments/machine-0", s.deployment)) 312 return senders 313 } 314 315 func (s *environSuite) networkInterfacesSender(nics ...network.Interface) *azuretesting.MockSender { 316 return s.makeSender(".*/networkInterfaces", network.InterfaceListResult{Value: &nics}) 317 } 318 319 func (s *environSuite) publicIPAddressesSender(pips ...network.PublicIPAddress) *azuretesting.MockSender { 320 return s.makeSender(".*/publicIPAddresses", network.PublicIPAddressListResult{Value: &pips}) 321 } 322 323 func (s *environSuite) virtualMachinesSender(vms ...compute.VirtualMachine) *azuretesting.MockSender { 324 return s.makeSender(".*/virtualMachines", compute.VirtualMachineListResult{Value: &vms}) 325 } 326 327 func (s *environSuite) vmSizesSender() *azuretesting.MockSender { 328 return s.makeSender(".*/vmSizes", s.vmSizes) 329 } 330 331 func (s *environSuite) storageAccountSender() *azuretesting.MockSender { 332 return s.makeSender(".*/storageAccounts/"+storageAccountName, s.storageAccount) 333 } 334 335 func (s *environSuite) storageAccountKeysSender() *azuretesting.MockSender { 336 return s.makeSender(".*/storageAccounts/.*/listKeys", s.storageAccountKeys) 337 } 338 339 func (s *environSuite) makeSender(pattern string, v interface{}) *azuretesting.MockSender { 340 sender := azuretesting.NewSenderWithValue(v) 341 sender.PathPattern = pattern 342 return sender 343 } 344 345 func makeStartInstanceParams(c *gc.C, controllerUUID, series string) environs.StartInstanceParams { 346 machineTag := names.NewMachineTag("0") 347 apiInfo := &api.Info{ 348 Addrs: []string{"localhost:17777"}, 349 CACert: testing.CACert, 350 Password: "admin", 351 Tag: machineTag, 352 ModelTag: testing.ModelTag, 353 } 354 355 icfg, err := instancecfg.NewInstanceConfig( 356 names.NewControllerTag(controllerUUID), 357 machineTag.Id(), "yanonce", imagemetadata.ReleasedStream, 358 series, apiInfo, 359 ) 360 c.Assert(err, jc.ErrorIsNil) 361 icfg.Tags = map[string]string{ 362 tags.JujuModel: testing.ModelTag.Id(), 363 tags.JujuController: controllerUUID, 364 } 365 366 return environs.StartInstanceParams{ 367 ControllerUUID: controllerUUID, 368 Tools: makeToolsList(series), 369 InstanceConfig: icfg, 370 } 371 } 372 373 func makeToolsList(series string) tools.List { 374 var toolsVersion version.Binary 375 toolsVersion.Number = version.MustParse("1.26.0") 376 toolsVersion.Arch = arch.AMD64 377 toolsVersion.Series = series 378 return tools.List{{ 379 Version: toolsVersion, 380 URL: fmt.Sprintf("http://example.com/tools/juju-%s.tgz", toolsVersion), 381 SHA256: "1234567890abcdef", 382 Size: 1024, 383 }} 384 } 385 386 func unmarshalRequestBody(c *gc.C, req *http.Request, out interface{}) { 387 bytes, err := ioutil.ReadAll(req.Body) 388 c.Assert(err, jc.ErrorIsNil) 389 err = json.Unmarshal(bytes, out) 390 c.Assert(err, jc.ErrorIsNil) 391 } 392 393 func assertRequestBody(c *gc.C, req *http.Request, expect interface{}) { 394 unmarshalled := reflect.New(reflect.TypeOf(expect).Elem()).Interface() 395 unmarshalRequestBody(c, req, unmarshalled) 396 c.Assert(unmarshalled, jc.DeepEquals, expect) 397 } 398 399 type mockClock struct { 400 gitjujutesting.Stub 401 *gitjujutesting.Clock 402 } 403 404 func (c *mockClock) After(d time.Duration) <-chan time.Time { 405 c.MethodCall(c, "After", d) 406 c.PopNoErr() 407 return c.Clock.After(d) 408 } 409 410 func (s *environSuite) TestOpen(c *gc.C) { 411 env := s.openEnviron(c) 412 c.Assert(env, gc.NotNil) 413 } 414 415 func (s *environSuite) TestCloudEndpointManagementURI(c *gc.C) { 416 env := s.openEnviron(c) 417 418 sender := mocks.NewSender() 419 sender.AppendResponse(mocks.NewResponseWithContent("{}")) 420 s.sender = azuretesting.Senders{sender} 421 s.requests = nil 422 env.AllInstances() // trigger a query 423 424 c.Assert(s.requests, gc.HasLen, 1) 425 c.Assert(s.requests[0].URL.Host, gc.Equals, "api.azurestack.local") 426 } 427 428 func (s *environSuite) TestStartInstance(c *gc.C) { 429 env := s.openEnviron(c) 430 s.sender = s.startInstanceSenders(false) 431 s.requests = nil 432 result, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal")) 433 c.Assert(err, jc.ErrorIsNil) 434 c.Assert(result, gc.NotNil) 435 c.Assert(result.Instance, gc.NotNil) 436 c.Assert(result.NetworkInfo, gc.HasLen, 0) 437 c.Assert(result.Volumes, gc.HasLen, 0) 438 c.Assert(result.VolumeAttachments, gc.HasLen, 0) 439 440 arch := "amd64" 441 mem := uint64(3584) 442 rootDisk := uint64(30 * 1024) // 30 GiB 443 cpuCores := uint64(1) 444 c.Assert(result.Hardware, jc.DeepEquals, &instance.HardwareCharacteristics{ 445 Arch: &arch, 446 Mem: &mem, 447 RootDisk: &rootDisk, 448 CpuCores: &cpuCores, 449 }) 450 s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{ 451 imageReference: &quantalImageReference, 452 diskSizeGB: 32, 453 osProfile: &linuxOsProfile, 454 }) 455 } 456 457 func (s *environSuite) TestStartInstanceWindowsMinRootDisk(c *gc.C) { 458 // The minimum OS disk size for Windows machines is 127GiB. 459 cons := constraints.MustParse("root-disk=44G") 460 s.testStartInstanceWindows(c, cons, 127*1024, 136) 461 } 462 463 func (s *environSuite) TestStartInstanceWindowsGrowableRootDisk(c *gc.C) { 464 // The OS disk size may be grown larger than 127GiB. 465 cons := constraints.MustParse("root-disk=200G") 466 s.testStartInstanceWindows(c, cons, 200*1024, 214) 467 } 468 469 func (s *environSuite) testStartInstanceWindows( 470 c *gc.C, cons constraints.Value, 471 expect uint64, requestValue int, 472 ) { 473 // Starting a Windows VM, we should not expect an image query. 474 s.PatchValue(&s.ubuntuServerSKUs, nil) 475 476 env := s.openEnviron(c) 477 s.sender = s.startInstanceSenders(false) 478 s.requests = nil 479 args := makeStartInstanceParams(c, s.controllerUUID, "win2012") 480 args.Constraints = cons 481 result, err := env.StartInstance(args) 482 c.Assert(err, jc.ErrorIsNil) 483 c.Assert(result, gc.NotNil) 484 c.Assert(result.Hardware.RootDisk, jc.DeepEquals, &expect) 485 486 vmExtensionSettings := map[string]interface{}{ 487 "commandToExecute": `` + 488 `move C:\AzureData\CustomData.bin C:\AzureData\CustomData.ps1 && ` + 489 `powershell.exe -ExecutionPolicy Unrestricted -File C:\AzureData\CustomData.ps1 && ` + 490 `del /q C:\AzureData\CustomData.ps1`, 491 } 492 s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{ 493 imageReference: &win2012ImageReference, 494 diskSizeGB: requestValue, 495 vmExtension: &compute.VirtualMachineExtensionProperties{ 496 Publisher: to.StringPtr("Microsoft.Compute"), 497 Type: to.StringPtr("CustomScriptExtension"), 498 TypeHandlerVersion: to.StringPtr("1.4"), 499 AutoUpgradeMinorVersion: to.BoolPtr(true), 500 Settings: &vmExtensionSettings, 501 }, 502 osProfile: &windowsOsProfile, 503 }) 504 } 505 506 func (s *environSuite) TestStartInstanceCentOS(c *gc.C) { 507 // Starting a CentOS VM, we should not expect an image query. 508 s.PatchValue(&s.ubuntuServerSKUs, nil) 509 510 env := s.openEnviron(c) 511 s.sender = s.startInstanceSenders(false) 512 s.requests = nil 513 args := makeStartInstanceParams(c, s.controllerUUID, "centos7") 514 _, err := env.StartInstance(args) 515 c.Assert(err, jc.ErrorIsNil) 516 517 vmExtensionSettings := map[string]interface{}{ 518 "commandToExecute": `bash -c 'base64 -d /var/lib/waagent/CustomData | bash'`, 519 } 520 s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{ 521 imageReference: ¢os7ImageReference, 522 diskSizeGB: 32, 523 vmExtension: &compute.VirtualMachineExtensionProperties{ 524 Publisher: to.StringPtr("Microsoft.OSTCExtensions"), 525 Type: to.StringPtr("CustomScriptForLinux"), 526 TypeHandlerVersion: to.StringPtr("1.4"), 527 AutoUpgradeMinorVersion: to.BoolPtr(true), 528 Settings: &vmExtensionSettings, 529 }, 530 osProfile: &linuxOsProfile, 531 }) 532 } 533 534 func (s *environSuite) TestStartInstanceTooManyRequests(c *gc.C) { 535 env := s.openEnviron(c) 536 senders := s.startInstanceSenders(false) 537 s.requests = nil 538 539 // 6 failures to get to 1 minute, and show that we cap it there. 540 const failures = 6 541 542 // Make the VirtualMachines.CreateOrUpdate call respond with 543 // 429 (StatusTooManyRequests) failures, and then with success. 544 rateLimitedSender := mocks.NewSender() 545 rateLimitedSender.AppendAndRepeatResponse(mocks.NewResponseWithBodyAndStatus( 546 mocks.NewBody("{}"), // empty JSON response to appease go-autorest 547 http.StatusTooManyRequests, 548 "(」゜ロ゜)」", 549 ), failures) 550 successSender := senders[len(senders)-1] 551 senders = senders[:len(senders)-1] 552 for i := 0; i < failures; i++ { 553 senders = append(senders, rateLimitedSender) 554 } 555 senders = append(senders, successSender) 556 s.sender = senders 557 558 _, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal")) 559 c.Assert(err, jc.ErrorIsNil) 560 561 c.Assert(s.requests, gc.HasLen, numExpectedStartInstanceRequests+failures) 562 s.assertStartInstanceRequests(c, s.requests[:numExpectedStartInstanceRequests], assertStartInstanceRequestsParams{ 563 imageReference: &quantalImageReference, 564 diskSizeGB: 32, 565 osProfile: &linuxOsProfile, 566 }) 567 568 // The final requests should all be identical. 569 for i := numExpectedStartInstanceRequests; i < numExpectedStartInstanceRequests+failures; i++ { 570 c.Assert(s.requests[i].Method, gc.Equals, "PUT") 571 c.Assert(s.requests[i].URL.Path, gc.Equals, s.requests[numExpectedStartInstanceRequests-1].URL.Path) 572 } 573 574 s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{ 575 {"After", []interface{}{5 * time.Second}}, 576 {"After", []interface{}{10 * time.Second}}, 577 {"After", []interface{}{20 * time.Second}}, 578 {"After", []interface{}{40 * time.Second}}, 579 {"After", []interface{}{1 * time.Minute}}, 580 {"After", []interface{}{1 * time.Minute}}, 581 }) 582 } 583 584 func (s *environSuite) TestStartInstanceTooManyRequestsTimeout(c *gc.C) { 585 env := s.openEnviron(c) 586 senders := s.startInstanceSenders(false) 587 s.requests = nil 588 589 // 8 failures to get to 5 minutes, which is as long as we'll keep 590 // retrying before giving up. 591 const failures = 8 592 593 // Make the VirtualMachines.Get call respond with enough 429 594 // (StatusTooManyRequests) failures to cause the method to give 595 // up retrying. 596 rateLimitedSender := mocks.NewSender() 597 rateLimitedSender.AppendAndRepeatResponse(mocks.NewResponseWithBodyAndStatus( 598 mocks.NewBody("{}"), // empty JSON response to appease go-autorest 599 http.StatusTooManyRequests, 600 "(」゜ロ゜)」", 601 ), failures) 602 senders = senders[:len(senders)-1] 603 for i := 0; i < failures; i++ { 604 senders = append(senders, rateLimitedSender) 605 } 606 s.sender = senders 607 608 _, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal")) 609 c.Assert(err, gc.ErrorMatches, `creating virtual machine "machine-0": creating deployment "machine-0": max duration exceeded: .*`) 610 611 s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{ 612 {"After", []interface{}{5 * time.Second}}, // t0 + 5s 613 {"After", []interface{}{10 * time.Second}}, // t0 + 15s 614 {"After", []interface{}{20 * time.Second}}, // t0 + 35s 615 {"After", []interface{}{40 * time.Second}}, // t0 + 1m15s 616 {"After", []interface{}{1 * time.Minute}}, // t0 + 2m15s 617 {"After", []interface{}{1 * time.Minute}}, // t0 + 3m15s 618 {"After", []interface{}{1 * time.Minute}}, // t0 + 4m15s 619 // There would be another call here, but since the time 620 // exceeds the give minute limit, retrying is aborted. 621 }) 622 } 623 624 func (s *environSuite) TestStartInstanceDistributionGroup(c *gc.C) { 625 c.Skip("TODO: test StartInstance's DistributionGroup behaviour") 626 } 627 628 func (s *environSuite) TestStartInstanceServiceAvailabilitySet(c *gc.C) { 629 env := s.openEnviron(c) 630 unitsDeployed := "mysql/0 wordpress/0" 631 s.vmTags[tags.JujuUnitsDeployed] = &unitsDeployed 632 s.sender = s.startInstanceSenders(false) 633 s.requests = nil 634 params := makeStartInstanceParams(c, s.controllerUUID, "quantal") 635 params.InstanceConfig.Tags[tags.JujuUnitsDeployed] = unitsDeployed 636 637 _, err := env.StartInstance(params) 638 c.Assert(err, jc.ErrorIsNil) 639 s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{ 640 availabilitySetName: "mysql", 641 imageReference: &quantalImageReference, 642 diskSizeGB: 32, 643 osProfile: &linuxOsProfile, 644 }) 645 } 646 647 const numExpectedStartInstanceRequests = 3 648 649 type assertStartInstanceRequestsParams struct { 650 availabilitySetName string 651 imageReference *compute.ImageReference 652 vmExtension *compute.VirtualMachineExtensionProperties 653 diskSizeGB int 654 osProfile *compute.OSProfile 655 } 656 657 func (s *environSuite) assertStartInstanceRequests( 658 c *gc.C, 659 requests []*http.Request, 660 args assertStartInstanceRequestsParams, 661 ) startInstanceRequests { 662 nsgId := `[resourceId('Microsoft.Network/networkSecurityGroups', 'juju-internal-nsg')]` 663 securityRules := []network.SecurityRule{{ 664 Name: to.StringPtr("SSHInbound"), 665 Properties: &network.SecurityRulePropertiesFormat{ 666 Description: to.StringPtr("Allow SSH access to all machines"), 667 Protocol: network.TCP, 668 SourceAddressPrefix: to.StringPtr("*"), 669 SourcePortRange: to.StringPtr("*"), 670 DestinationAddressPrefix: to.StringPtr("*"), 671 DestinationPortRange: to.StringPtr("22"), 672 Access: network.Allow, 673 Priority: to.Int32Ptr(100), 674 Direction: network.Inbound, 675 }, 676 }, { 677 Name: to.StringPtr("JujuAPIInbound"), 678 Properties: &network.SecurityRulePropertiesFormat{ 679 Description: to.StringPtr("Allow API connections to controller machines"), 680 Protocol: network.TCP, 681 SourceAddressPrefix: to.StringPtr("*"), 682 SourcePortRange: to.StringPtr("*"), 683 DestinationAddressPrefix: to.StringPtr("192.168.16.0/20"), 684 DestinationPortRange: to.StringPtr("17777"), 685 Access: network.Allow, 686 Priority: to.Int32Ptr(101), 687 Direction: network.Inbound, 688 }, 689 }} 690 subnets := []network.Subnet{{ 691 Name: to.StringPtr("juju-internal-subnet"), 692 Properties: &network.SubnetPropertiesFormat{ 693 AddressPrefix: to.StringPtr("192.168.0.0/20"), 694 NetworkSecurityGroup: &network.SecurityGroup{ 695 ID: to.StringPtr(nsgId), 696 }, 697 }, 698 }, { 699 Name: to.StringPtr("juju-controller-subnet"), 700 Properties: &network.SubnetPropertiesFormat{ 701 AddressPrefix: to.StringPtr("192.168.16.0/20"), 702 NetworkSecurityGroup: &network.SecurityGroup{ 703 ID: to.StringPtr(nsgId), 704 }, 705 }, 706 }} 707 708 subnetName := "juju-internal-subnet" 709 privateIPAddress := "192.168.0.4" 710 if args.availabilitySetName == "juju-controller" { 711 subnetName = "juju-controller-subnet" 712 privateIPAddress = "192.168.16.4" 713 } 714 subnetId := fmt.Sprintf( 715 `[concat(resourceId('Microsoft.Network/virtualNetworks', 'juju-internal-network'), '/subnets/%s')]`, 716 subnetName, 717 ) 718 719 publicIPAddressId := `[resourceId('Microsoft.Network/publicIPAddresses', 'machine-0-public-ip')]` 720 721 ipConfigurations := []network.InterfaceIPConfiguration{{ 722 Name: to.StringPtr("primary"), 723 Properties: &network.InterfaceIPConfigurationPropertiesFormat{ 724 Primary: to.BoolPtr(true), 725 PrivateIPAddress: to.StringPtr(privateIPAddress), 726 PrivateIPAllocationMethod: network.Static, 727 Subnet: &network.Subnet{ID: to.StringPtr(subnetId)}, 728 PublicIPAddress: &network.PublicIPAddress{ 729 ID: to.StringPtr(publicIPAddressId), 730 }, 731 }, 732 }} 733 734 nicId := `[resourceId('Microsoft.Network/networkInterfaces', 'machine-0-primary')]` 735 nics := []compute.NetworkInterfaceReference{{ 736 ID: to.StringPtr(nicId), 737 Properties: &compute.NetworkInterfaceReferenceProperties{ 738 Primary: to.BoolPtr(true), 739 }, 740 }} 741 vmDependsOn := []string{ 742 nicId, 743 `[resourceId('Microsoft.Storage/storageAccounts', '` + storageAccountName + `')]`, 744 } 745 746 addressPrefixes := []string{"192.168.0.0/20", "192.168.16.0/20"} 747 templateResources := []armtemplates.Resource{{ 748 APIVersion: network.APIVersion, 749 Type: "Microsoft.Network/networkSecurityGroups", 750 Name: "juju-internal-nsg", 751 Location: "westus", 752 Tags: to.StringMap(s.envTags), 753 Properties: &network.SecurityGroupPropertiesFormat{ 754 SecurityRules: &securityRules, 755 }, 756 }, { 757 APIVersion: network.APIVersion, 758 Type: "Microsoft.Network/virtualNetworks", 759 Name: "juju-internal-network", 760 Location: "westus", 761 Tags: to.StringMap(s.envTags), 762 Properties: &network.VirtualNetworkPropertiesFormat{ 763 AddressSpace: &network.AddressSpace{&addressPrefixes}, 764 Subnets: &subnets, 765 }, 766 DependsOn: []string{nsgId}, 767 }, { 768 APIVersion: storage.APIVersion, 769 Type: "Microsoft.Storage/storageAccounts", 770 Name: storageAccountName, 771 Location: "westus", 772 Tags: to.StringMap(s.envTags), 773 StorageSku: &storage.Sku{ 774 Name: storage.SkuName("Standard_LRS"), 775 }, 776 }} 777 778 var availabilitySetSubResource *compute.SubResource 779 if args.availabilitySetName != "" { 780 availabilitySetId := fmt.Sprintf( 781 `[resourceId('Microsoft.Compute/availabilitySets','%s')]`, 782 args.availabilitySetName, 783 ) 784 templateResources = append(templateResources, armtemplates.Resource{ 785 APIVersion: compute.APIVersion, 786 Type: "Microsoft.Compute/availabilitySets", 787 Name: args.availabilitySetName, 788 Location: "westus", 789 Tags: to.StringMap(s.envTags), 790 }) 791 availabilitySetSubResource = &compute.SubResource{ 792 ID: to.StringPtr(availabilitySetId), 793 } 794 vmDependsOn = append([]string{availabilitySetId}, vmDependsOn...) 795 } 796 797 templateResources = append(templateResources, []armtemplates.Resource{{ 798 APIVersion: network.APIVersion, 799 Type: "Microsoft.Network/publicIPAddresses", 800 Name: "machine-0-public-ip", 801 Location: "westus", 802 Tags: to.StringMap(s.vmTags), 803 Properties: &network.PublicIPAddressPropertiesFormat{ 804 PublicIPAllocationMethod: network.Dynamic, 805 }, 806 }, { 807 APIVersion: network.APIVersion, 808 Type: "Microsoft.Network/networkInterfaces", 809 Name: "machine-0-primary", 810 Location: "westus", 811 Tags: to.StringMap(s.vmTags), 812 Properties: &network.InterfacePropertiesFormat{ 813 IPConfigurations: &ipConfigurations, 814 }, 815 DependsOn: []string{ 816 publicIPAddressId, 817 `[resourceId('Microsoft.Network/virtualNetworks', 'juju-internal-network')]`, 818 }, 819 }, { 820 APIVersion: compute.APIVersion, 821 Type: "Microsoft.Compute/virtualMachines", 822 Name: "machine-0", 823 Location: "westus", 824 Tags: to.StringMap(s.vmTags), 825 Properties: &compute.VirtualMachineProperties{ 826 HardwareProfile: &compute.HardwareProfile{ 827 VMSize: "Standard_D1", 828 }, 829 StorageProfile: &compute.StorageProfile{ 830 ImageReference: args.imageReference, 831 OsDisk: &compute.OSDisk{ 832 Name: to.StringPtr("machine-0"), 833 CreateOption: compute.FromImage, 834 Caching: compute.ReadWrite, 835 Vhd: &compute.VirtualHardDisk{ 836 URI: to.StringPtr(fmt.Sprintf( 837 `[concat(reference(resourceId('Microsoft.Storage/storageAccounts', '%s'), '%s').primaryEndpoints.blob, 'osvhds/machine-0.vhd')]`, 838 storageAccountName, storage.APIVersion, 839 )), 840 }, 841 DiskSizeGB: to.Int32Ptr(int32(args.diskSizeGB)), 842 }, 843 }, 844 OsProfile: args.osProfile, 845 NetworkProfile: &compute.NetworkProfile{&nics}, 846 AvailabilitySet: availabilitySetSubResource, 847 }, 848 DependsOn: vmDependsOn, 849 }}...) 850 if args.vmExtension != nil { 851 templateResources = append(templateResources, armtemplates.Resource{ 852 APIVersion: compute.APIVersion, 853 Type: "Microsoft.Compute/virtualMachines/extensions", 854 Name: "machine-0/JujuCustomScriptExtension", 855 Location: "westus", 856 Tags: to.StringMap(s.vmTags), 857 Properties: args.vmExtension, 858 DependsOn: []string{"Microsoft.Compute/virtualMachines/machine-0"}, 859 }) 860 } 861 templateMap := map[string]interface{}{ 862 "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 863 "contentVersion": "1.0.0.0", 864 "resources": templateResources, 865 } 866 deployment := &resources.Deployment{ 867 &resources.DeploymentProperties{ 868 Template: &templateMap, 869 Mode: resources.Incremental, 870 }, 871 } 872 873 // Validate HTTP request bodies. 874 var startInstanceRequests startInstanceRequests 875 if args.vmExtension != nil { 876 // It must be Windows or CentOS, so 877 // there should be no image query. 878 c.Assert(requests, gc.HasLen, numExpectedStartInstanceRequests-1) 879 c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes 880 c.Assert(requests[1].Method, gc.Equals, "PUT") // create deployment 881 startInstanceRequests.vmSizes = requests[0] 882 startInstanceRequests.deployment = requests[1] 883 } else { 884 c.Assert(requests, gc.HasLen, numExpectedStartInstanceRequests) 885 c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes 886 c.Assert(requests[1].Method, gc.Equals, "GET") // skus 887 c.Assert(requests[2].Method, gc.Equals, "PUT") // create deployment 888 startInstanceRequests.vmSizes = requests[0] 889 startInstanceRequests.skus = requests[1] 890 startInstanceRequests.deployment = requests[2] 891 } 892 893 // Marshal/unmarshal the deployment we expect, so it's in map form. 894 var expected resources.Deployment 895 data, err := json.Marshal(&deployment) 896 c.Assert(err, jc.ErrorIsNil) 897 err = json.Unmarshal(data, &expected) 898 c.Assert(err, jc.ErrorIsNil) 899 900 // Check that we send what we expect. CustomData is non-deterministic, 901 // so don't compare it. 902 // TODO(axw) shouldn't CustomData be deterministic? Look into this. 903 var actual resources.Deployment 904 unmarshalRequestBody(c, startInstanceRequests.deployment, &actual) 905 c.Assert(actual.Properties, gc.NotNil) 906 c.Assert(actual.Properties.Template, gc.NotNil) 907 resources := (*actual.Properties.Template)["resources"].([]interface{}) 908 c.Assert(resources, gc.HasLen, len(templateResources)) 909 910 vmResourceIndex := len(resources) - 1 911 if args.vmExtension != nil { 912 vmResourceIndex-- 913 } 914 vmResource := resources[vmResourceIndex].(map[string]interface{}) 915 vmResourceProperties := vmResource["properties"].(map[string]interface{}) 916 osProfile := vmResourceProperties["osProfile"].(map[string]interface{}) 917 osProfile["customData"] = "<juju-goes-here>" 918 c.Assert(actual, jc.DeepEquals, expected) 919 920 return startInstanceRequests 921 } 922 923 type startInstanceRequests struct { 924 vmSizes *http.Request 925 skus *http.Request 926 deployment *http.Request 927 } 928 929 func (s *environSuite) TestBootstrap(c *gc.C) { 930 defer envtesting.DisableFinishBootstrap()() 931 932 ctx := envtesting.BootstrapContext(c) 933 env := prepareForBootstrap(c, ctx, s.provider, &s.sender) 934 935 s.sender = s.initResourceGroupSenders() 936 s.sender = append(s.sender, s.startInstanceSenders(true)...) 937 s.requests = nil 938 result, err := env.Bootstrap( 939 ctx, environs.BootstrapParams{ 940 ControllerConfig: testing.FakeControllerConfig(), 941 AvailableTools: makeToolsList("quantal"), 942 BootstrapSeries: "quantal", 943 }, 944 ) 945 c.Assert(err, jc.ErrorIsNil) 946 c.Assert(result.Arch, gc.Equals, "amd64") 947 c.Assert(result.Series, gc.Equals, "quantal") 948 949 c.Assert(len(s.requests), gc.Equals, numExpectedStartInstanceRequests+1) 950 s.vmTags[tags.JujuIsController] = to.StringPtr("true") 951 s.assertStartInstanceRequests(c, s.requests[1:], assertStartInstanceRequestsParams{ 952 availabilitySetName: "juju-controller", 953 imageReference: &quantalImageReference, 954 diskSizeGB: 32, 955 osProfile: &linuxOsProfile, 956 }) 957 } 958 959 func (s *environSuite) TestAllInstancesResourceGroupNotFound(c *gc.C) { 960 env := s.openEnviron(c) 961 sender := mocks.NewSender() 962 sender.AppendResponse(mocks.NewResponseWithStatus( 963 "resource group not found", http.StatusNotFound, 964 )) 965 s.sender = azuretesting.Senders{sender} 966 _, err := env.AllInstances() 967 c.Assert(err, jc.ErrorIsNil) 968 } 969 970 func (s *environSuite) TestStopInstancesNotFound(c *gc.C) { 971 env := s.openEnviron(c) 972 sender0 := mocks.NewSender() 973 sender0.AppendResponse(mocks.NewResponseWithStatus( 974 "vm not found", http.StatusNotFound, 975 )) 976 sender1 := mocks.NewSender() 977 sender1.AppendResponse(mocks.NewResponseWithStatus( 978 "vm not found", http.StatusNotFound, 979 )) 980 s.sender = azuretesting.Senders{sender0, sender1} 981 err := env.StopInstances("a", "b") 982 c.Assert(err, jc.ErrorIsNil) 983 } 984 985 func (s *environSuite) TestStopInstances(c *gc.C) { 986 env := s.openEnviron(c) 987 988 // Security group has rules for machine-0, as well as a rule that doesn't match. 989 nsg := makeSecurityGroup( 990 makeSecurityRule("machine-0-80", "192.168.0.4", "80"), 991 makeSecurityRule("machine-0-1000-2000", "192.168.0.4", "1000-2000"), 992 makeSecurityRule("machine-42", "192.168.0.5", "*"), 993 ) 994 995 // Create an IP configuration with a public IP reference. This will 996 // cause an update to the NIC to detach public IPs. 997 nic0IPConfiguration := makeIPConfiguration("192.168.0.4") 998 nic0IPConfiguration.Properties.PublicIPAddress = &network.PublicIPAddress{} 999 nic0 := makeNetworkInterface("nic-0", "machine-0", nic0IPConfiguration) 1000 1001 s.sender = azuretesting.Senders{ 1002 s.makeSender(".*/deployments/machine-0/cancel", nil), // POST 1003 s.storageAccountSender(), 1004 s.storageAccountKeysSender(), 1005 s.networkInterfacesSender(nic0), 1006 s.publicIPAddressesSender(makePublicIPAddress("pip-0", "machine-0", "1.2.3.4")), 1007 s.makeSender(".*/virtualMachines/machine-0", nil), // DELETE 1008 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg), // GET 1009 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-80", nil), // DELETE 1010 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-1000-2000", nil), // DELETE 1011 s.makeSender(".*/networkInterfaces/nic-0", nil), // DELETE 1012 s.makeSender(".*/publicIPAddresses/pip-0", nil), // DELETE 1013 s.makeSender(".*/deployments/machine-0", nil), // DELETE 1014 } 1015 err := env.StopInstances("machine-0") 1016 c.Assert(err, jc.ErrorIsNil) 1017 1018 s.storageClient.CheckCallNames(c, 1019 "NewClient", "DeleteBlobIfExists", 1020 ) 1021 s.storageClient.CheckCall(c, 1, "DeleteBlobIfExists", "osvhds", "machine-0") 1022 } 1023 1024 func (s *environSuite) TestStopInstancesMultiple(c *gc.C) { 1025 env := s.openEnviron(c) 1026 1027 vmDeleteSender0 := s.makeSender(".*/virtualMachines/machine-[01]", nil) 1028 vmDeleteSender1 := s.makeSender(".*/virtualMachines/machine-[01]", nil) 1029 vmDeleteSender0.SetError(errors.New("blargh")) 1030 vmDeleteSender1.SetError(errors.New("blargh")) 1031 1032 s.sender = azuretesting.Senders{ 1033 s.makeSender(".*/deployments/machine-[01]/cancel", nil), // POST 1034 s.makeSender(".*/deployments/machine-[01]/cancel", nil), // POST 1035 1036 // We should only query the NICs, public IPs, and storage 1037 // account/keys, regardless of how many instances are deleted. 1038 s.storageAccountSender(), 1039 s.storageAccountKeysSender(), 1040 s.networkInterfacesSender(), 1041 s.publicIPAddressesSender(), 1042 1043 vmDeleteSender0, 1044 vmDeleteSender1, 1045 } 1046 err := env.StopInstances("machine-0", "machine-1") 1047 c.Assert(err, gc.ErrorMatches, `deleting instance "machine-[01]":.*blargh`) 1048 } 1049 1050 func (s *environSuite) TestStopInstancesDeploymentNotFound(c *gc.C) { 1051 env := s.openEnviron(c) 1052 1053 cancelSender := mocks.NewSender() 1054 cancelSender.AppendResponse(mocks.NewResponseWithStatus( 1055 "deployment not found", http.StatusNotFound, 1056 )) 1057 s.sender = azuretesting.Senders{cancelSender} 1058 err := env.StopInstances("machine-0") 1059 c.Assert(err, jc.ErrorIsNil) 1060 } 1061 1062 func (s *environSuite) TestStopInstancesStorageAccountNoKeys(c *gc.C) { 1063 s.PatchValue(&s.storageAccountKeys.Keys, nil) 1064 s.testStopInstancesStorageAccountNotFound(c) 1065 } 1066 1067 func (s *environSuite) TestStopInstancesStorageAccountNoFullKey(c *gc.C) { 1068 keys := *s.storageAccountKeys.Keys 1069 s.PatchValue(&keys[0].Permissions, storage.READ) 1070 s.testStopInstancesStorageAccountNotFound(c) 1071 } 1072 1073 func (s *environSuite) testStopInstancesStorageAccountNotFound(c *gc.C) { 1074 env := s.openEnviron(c) 1075 s.sender = azuretesting.Senders{ 1076 s.makeSender("/deployments/machine-0", s.deployment), // Cancel 1077 s.storageAccountSender(), 1078 s.storageAccountKeysSender(), 1079 s.networkInterfacesSender(), // GET: no NICs 1080 s.publicIPAddressesSender(), // GET: no public IPs 1081 s.makeSender(".*/virtualMachines/machine-0", nil), // DELETE 1082 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", makeSecurityGroup()), // GET: no rules 1083 s.makeSender(".*/deployments/machine-0", nil), // DELETE 1084 } 1085 err := env.StopInstances("machine-0") 1086 c.Assert(err, jc.ErrorIsNil) 1087 } 1088 1089 func (s *environSuite) TestStopInstancesStorageAccountError(c *gc.C) { 1090 env := s.openEnviron(c) 1091 errorSender := s.storageAccountSender() 1092 errorSender.SetError(errors.New("blargh")) 1093 s.sender = azuretesting.Senders{ 1094 s.makeSender("/deployments/machine-0", s.deployment), // Cancel 1095 errorSender, 1096 } 1097 err := env.StopInstances("machine-0") 1098 c.Assert(err, gc.ErrorMatches, "getting storage account:.*blargh") 1099 } 1100 1101 func (s *environSuite) TestStopInstancesStorageAccountKeysError(c *gc.C) { 1102 env := s.openEnviron(c) 1103 errorSender := s.storageAccountKeysSender() 1104 errorSender.SetError(errors.New("blargh")) 1105 s.sender = azuretesting.Senders{ 1106 s.makeSender("/deployments/machine-0", s.deployment), // Cancel 1107 s.storageAccountSender(), 1108 errorSender, 1109 } 1110 err := env.StopInstances("machine-0") 1111 c.Assert(err, gc.ErrorMatches, "getting storage account key:.*blargh") 1112 } 1113 1114 func (s *environSuite) TestConstraintsValidatorUnsupported(c *gc.C) { 1115 validator := s.constraintsValidator(c) 1116 unsupported, err := validator.Validate(constraints.MustParse( 1117 "arch=amd64 tags=foo cpu-power=100 virt-type=kvm", 1118 )) 1119 c.Assert(err, jc.ErrorIsNil) 1120 c.Assert(unsupported, jc.SameContents, []string{"tags", "cpu-power", "virt-type"}) 1121 } 1122 1123 func (s *environSuite) TestConstraintsValidatorVocabulary(c *gc.C) { 1124 validator := s.constraintsValidator(c) 1125 _, err := validator.Validate(constraints.MustParse("arch=armhf")) 1126 c.Assert(err, gc.ErrorMatches, 1127 "invalid constraint value: arch=armhf\nvalid values are: \\[amd64\\]", 1128 ) 1129 _, err = validator.Validate(constraints.MustParse("instance-type=t1.micro")) 1130 c.Assert(err, gc.ErrorMatches, 1131 "invalid constraint value: instance-type=t1.micro\nvalid values are: \\[D1 Standard_D1\\]", 1132 ) 1133 } 1134 1135 func (s *environSuite) TestConstraintsValidatorMerge(c *gc.C) { 1136 validator := s.constraintsValidator(c) 1137 cons, err := validator.Merge( 1138 constraints.MustParse("mem=3G arch=amd64"), 1139 constraints.MustParse("instance-type=D1"), 1140 ) 1141 c.Assert(err, jc.ErrorIsNil) 1142 c.Assert(cons.String(), gc.Equals, "instance-type=D1") 1143 } 1144 1145 func (s *environSuite) constraintsValidator(c *gc.C) constraints.Validator { 1146 env := s.openEnviron(c) 1147 s.sender = azuretesting.Senders{s.vmSizesSender()} 1148 validator, err := env.ConstraintsValidator() 1149 c.Assert(err, jc.ErrorIsNil) 1150 return validator 1151 } 1152 1153 func (s *environSuite) TestAgentMirror(c *gc.C) { 1154 env := s.openEnviron(c) 1155 c.Assert(env, gc.Implements, new(envtools.HasAgentMirror)) 1156 cloudSpec, err := env.(envtools.HasAgentMirror).AgentMirror() 1157 c.Assert(err, jc.ErrorIsNil) 1158 c.Assert(cloudSpec, gc.Equals, simplestreams.CloudSpec{ 1159 Region: "westus", 1160 Endpoint: "https://storage.azurestack.local/", 1161 }) 1162 } 1163 1164 func (s *environSuite) TestDestroyHostedModel(c *gc.C) { 1165 env := s.openEnviron(c, testing.Attrs{"controller-uuid": utils.MustNewUUID().String()}) 1166 s.sender = azuretesting.Senders{ 1167 s.makeSender(".*/resourcegroups/juju-testenv-model-"+testing.ModelTag.Id(), nil), // DELETE 1168 } 1169 err := env.Destroy() 1170 c.Assert(err, jc.ErrorIsNil) 1171 c.Assert(s.requests, gc.HasLen, 1) 1172 c.Assert(s.requests[0].Method, gc.Equals, "DELETE") 1173 } 1174 1175 func (s *environSuite) TestDestroyController(c *gc.C) { 1176 groups := []resources.ResourceGroup{{ 1177 Name: to.StringPtr("group1"), 1178 }, { 1179 Name: to.StringPtr("group2"), 1180 }} 1181 result := resources.ResourceGroupListResult{Value: &groups} 1182 1183 env := s.openEnviron(c) 1184 s.sender = azuretesting.Senders{ 1185 s.makeSender(".*/resourcegroups", result), // GET 1186 s.makeSender(".*/resourcegroups/group[12]", nil), // DELETE 1187 s.makeSender(".*/resourcegroups/group[12]", nil), // DELETE 1188 } 1189 err := env.DestroyController(s.controllerUUID) 1190 c.Assert(err, jc.ErrorIsNil) 1191 1192 c.Assert(s.requests, gc.HasLen, 3) 1193 c.Assert(s.requests[0].Method, gc.Equals, "GET") 1194 c.Assert(s.requests[0].URL.Query().Get("$filter"), gc.Equals, fmt.Sprintf( 1195 "tagname eq 'juju-controller-uuid' and tagvalue eq '%s'", 1196 testing.ControllerTag.Id(), 1197 )) 1198 c.Assert(s.requests[1].Method, gc.Equals, "DELETE") 1199 c.Assert(s.requests[2].Method, gc.Equals, "DELETE") 1200 1201 // Groups are deleted concurrently, so there's no known order. 1202 groupsDeleted := []string{ 1203 path.Base(s.requests[1].URL.Path), 1204 path.Base(s.requests[2].URL.Path), 1205 } 1206 c.Assert(groupsDeleted, jc.SameContents, []string{"group1", "group2"}) 1207 } 1208 1209 func (s *environSuite) TestDestroyControllerErrors(c *gc.C) { 1210 groups := []resources.ResourceGroup{ 1211 {Name: to.StringPtr("group1")}, 1212 {Name: to.StringPtr("group2")}, 1213 } 1214 result := resources.ResourceGroupListResult{Value: &groups} 1215 1216 makeErrorSender := func(err string) *azuretesting.MockSender { 1217 errorSender := &azuretesting.MockSender{ 1218 Sender: mocks.NewSender(), 1219 PathPattern: ".*/resourcegroups/group[12].*", 1220 } 1221 errorSender.SetError(errors.New(err)) 1222 return errorSender 1223 } 1224 1225 env := s.openEnviron(c) 1226 s.requests = nil 1227 s.sender = azuretesting.Senders{ 1228 s.makeSender(".*/resourcegroups", result), // GET 1229 makeErrorSender("foo"), // DELETE 1230 makeErrorSender("bar"), // DELETE 1231 } 1232 destroyErr := env.DestroyController(s.controllerUUID) 1233 // checked below, once we know the order of deletions. 1234 1235 c.Assert(s.requests, gc.HasLen, 3) 1236 c.Assert(s.requests[0].Method, gc.Equals, "GET") 1237 c.Assert(s.requests[1].Method, gc.Equals, "DELETE") 1238 c.Assert(s.requests[2].Method, gc.Equals, "DELETE") 1239 1240 // Groups are deleted concurrently, so there's no known order. 1241 groupsDeleted := []string{ 1242 path.Base(s.requests[1].URL.Path), 1243 path.Base(s.requests[2].URL.Path), 1244 } 1245 c.Assert(groupsDeleted, jc.SameContents, []string{"group1", "group2"}) 1246 1247 c.Check(destroyErr, gc.ErrorMatches, 1248 `deleting resource group "group1":.*; `+ 1249 `deleting resource group "group2":.*`) 1250 c.Check(destroyErr, gc.ErrorMatches, ".*foo.*") 1251 c.Check(destroyErr, gc.ErrorMatches, ".*bar.*") 1252 }