github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/provider/azure/environ_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 "bytes" 8 "encoding/base64" 9 "encoding/xml" 10 "fmt" 11 "io/ioutil" 12 "net/http" 13 "net/url" 14 "path" 15 "regexp" 16 "strings" 17 "sync" 18 19 "github.com/juju/testing" 20 jc "github.com/juju/testing/checkers" 21 gc "launchpad.net/gocheck" 22 "launchpad.net/gwacl" 23 24 "github.com/juju/juju/constraints" 25 "github.com/juju/juju/environs" 26 "github.com/juju/juju/environs/bootstrap" 27 "github.com/juju/juju/environs/config" 28 "github.com/juju/juju/environs/imagemetadata" 29 "github.com/juju/juju/environs/instances" 30 "github.com/juju/juju/environs/simplestreams" 31 "github.com/juju/juju/environs/storage" 32 envtesting "github.com/juju/juju/environs/testing" 33 "github.com/juju/juju/environs/tools" 34 "github.com/juju/juju/instance" 35 "github.com/juju/juju/mongo" 36 "github.com/juju/juju/state" 37 "github.com/juju/juju/state/api" 38 apiparams "github.com/juju/juju/state/api/params" 39 coretesting "github.com/juju/juju/testing" 40 ) 41 42 type baseEnvironSuite struct { 43 providerSuite 44 } 45 46 type environSuite struct { 47 baseEnvironSuite 48 } 49 50 var _ = gc.Suite(&environSuite{}) 51 var _ = gc.Suite(&startInstanceSuite{}) 52 53 // makeEnviron creates a fake azureEnviron with arbitrary configuration. 54 func makeEnviron(c *gc.C) *azureEnviron { 55 attrs := makeAzureConfigMap(c) 56 return makeEnvironWithConfig(c, attrs) 57 } 58 59 // makeEnvironWithConfig creates a fake azureEnviron with the specified configuration. 60 func makeEnvironWithConfig(c *gc.C, attrs map[string]interface{}) *azureEnviron { 61 cfg, err := config.New(config.NoDefaults, attrs) 62 c.Assert(err, gc.IsNil) 63 env, err := NewEnviron(cfg) 64 c.Assert(err, gc.IsNil) 65 // Prevent the test from trying to query for a storage-account key. 66 env.storageAccountKey = "fake-storage-account-key" 67 return env 68 } 69 70 // setDummyStorage injects the local provider's fake storage implementation 71 // into the given environment, so that tests can manipulate storage as if it 72 // were real. 73 func (s *baseEnvironSuite) setDummyStorage(c *gc.C, env *azureEnviron) { 74 closer, storage, _ := envtesting.CreateLocalTestStorage(c) 75 env.storage = storage 76 s.AddCleanup(func(c *gc.C) { closer.Close() }) 77 } 78 79 func (*environSuite) TestGetEndpoint(c *gc.C) { 80 c.Check( 81 getEndpoint("West US"), 82 gc.Equals, 83 "https://management.core.windows.net/") 84 c.Check( 85 getEndpoint("China East"), 86 gc.Equals, 87 "https://management.core.chinacloudapi.cn/") 88 } 89 90 func (*environSuite) TestGetSnapshot(c *gc.C) { 91 original := azureEnviron{name: "this-env", ecfg: new(azureEnvironConfig)} 92 snapshot := original.getSnapshot() 93 94 // The snapshot is identical to the original. 95 c.Check(*snapshot, gc.DeepEquals, original) 96 97 // However, they are distinct objects. 98 c.Check(snapshot, gc.Not(gc.Equals), &original) 99 100 // It's a shallow copy; they still share pointers. 101 c.Check(snapshot.ecfg, gc.Equals, original.ecfg) 102 103 // Neither object is locked at the end of the copy. 104 c.Check(original.Mutex, gc.Equals, sync.Mutex{}) 105 c.Check(snapshot.Mutex, gc.Equals, sync.Mutex{}) 106 } 107 108 func (*environSuite) TestGetSnapshotLocksEnviron(c *gc.C) { 109 original := azureEnviron{} 110 coretesting.TestLockingFunction(&original.Mutex, func() { original.getSnapshot() }) 111 } 112 113 func (*environSuite) TestName(c *gc.C) { 114 env := azureEnviron{name: "foo"} 115 c.Check(env.Name(), gc.Equals, env.name) 116 } 117 118 func (*environSuite) TestConfigReturnsConfig(c *gc.C) { 119 cfg := new(config.Config) 120 ecfg := azureEnvironConfig{Config: cfg} 121 env := azureEnviron{ecfg: &ecfg} 122 c.Check(env.Config(), gc.Equals, cfg) 123 } 124 125 func (*environSuite) TestConfigLocksEnviron(c *gc.C) { 126 env := azureEnviron{name: "env", ecfg: new(azureEnvironConfig)} 127 coretesting.TestLockingFunction(&env.Mutex, func() { env.Config() }) 128 } 129 130 func (*environSuite) TestGetManagementAPI(c *gc.C) { 131 env := makeEnviron(c) 132 context, err := env.getManagementAPI() 133 c.Assert(err, gc.IsNil) 134 defer env.releaseManagementAPI(context) 135 c.Check(context, gc.NotNil) 136 c.Check(context.ManagementAPI, gc.NotNil) 137 c.Check(context.certFile, gc.NotNil) 138 c.Check(context.GetRetryPolicy(), gc.DeepEquals, retryPolicy) 139 } 140 141 func (*environSuite) TestReleaseManagementAPIAcceptsNil(c *gc.C) { 142 env := makeEnviron(c) 143 env.releaseManagementAPI(nil) 144 // The real test is that this does not panic. 145 } 146 147 func (*environSuite) TestReleaseManagementAPIAcceptsIncompleteContext(c *gc.C) { 148 env := makeEnviron(c) 149 context := azureManagementContext{ 150 ManagementAPI: nil, 151 certFile: nil, 152 } 153 env.releaseManagementAPI(&context) 154 // The real test is that this does not panic. 155 } 156 157 func getAzureServiceListResponse(c *gc.C, services ...gwacl.HostedServiceDescriptor) []gwacl.DispatcherResponse { 158 list := gwacl.HostedServiceDescriptorList{HostedServices: services} 159 listXML, err := list.Serialize() 160 c.Assert(err, gc.IsNil) 161 responses := []gwacl.DispatcherResponse{gwacl.NewDispatcherResponse( 162 []byte(listXML), 163 http.StatusOK, 164 nil, 165 )} 166 return responses 167 } 168 169 // getAzureServiceResponse returns a gwacl.DispatcherResponse corresponding 170 // to the API request used to get the properties of a Service. 171 func getAzureServiceResponse(c *gc.C, service gwacl.HostedService) gwacl.DispatcherResponse { 172 serviceXML, err := service.Serialize() 173 c.Assert(err, gc.IsNil) 174 return gwacl.NewDispatcherResponse([]byte(serviceXML), http.StatusOK, nil) 175 } 176 177 func patchWithServiceListResponse(c *gc.C, services []gwacl.HostedServiceDescriptor) *[]*gwacl.X509Request { 178 responses := getAzureServiceListResponse(c, services...) 179 return gwacl.PatchManagementAPIResponses(responses) 180 } 181 182 func prepareInstancesResponses(c *gc.C, prefix string, services ...*gwacl.HostedService) []gwacl.DispatcherResponse { 183 descriptors := make([]gwacl.HostedServiceDescriptor, len(services)) 184 for i, service := range services { 185 descriptors[i] = service.HostedServiceDescriptor 186 } 187 responses := getAzureServiceListResponse(c, descriptors...) 188 for _, service := range services { 189 if !strings.HasPrefix(service.ServiceName, prefix) { 190 continue 191 } 192 serviceXML, err := service.Serialize() 193 c.Assert(err, gc.IsNil) 194 serviceGetResponse := gwacl.NewDispatcherResponse([]byte(serviceXML), http.StatusOK, nil) 195 responses = append(responses, serviceGetResponse) 196 } 197 return responses 198 } 199 200 func patchInstancesResponses(c *gc.C, prefix string, services ...*gwacl.HostedService) *[]*gwacl.X509Request { 201 responses := prepareInstancesResponses(c, prefix, services...) 202 return gwacl.PatchManagementAPIResponses(responses) 203 } 204 205 func (s *environSuite) TestSupportedArchitectures(c *gc.C) { 206 env := s.setupEnvWithDummyMetadata(c) 207 a, err := env.SupportedArchitectures() 208 c.Assert(err, gc.IsNil) 209 c.Assert(a, gc.DeepEquals, []string{"amd64"}) 210 } 211 212 func (s *environSuite) TestSupportNetworks(c *gc.C) { 213 env := s.setupEnvWithDummyMetadata(c) 214 c.Assert(env.SupportNetworks(), jc.IsFalse) 215 } 216 217 func (suite *environSuite) TestGetEnvPrefixContainsEnvName(c *gc.C) { 218 env := makeEnviron(c) 219 c.Check(strings.Contains(env.getEnvPrefix(), env.Name()), jc.IsTrue) 220 } 221 222 func (*environSuite) TestGetContainerName(c *gc.C) { 223 env := makeEnviron(c) 224 expected := env.getEnvPrefix() + "private" 225 c.Check(env.getContainerName(), gc.Equals, expected) 226 } 227 228 func (suite *environSuite) TestAllInstances(c *gc.C) { 229 env := makeEnviron(c) 230 prefix := env.getEnvPrefix() 231 service1 := makeLegacyDeployment(env, prefix+"service1") 232 service2 := makeDeployment(env, prefix+"service2") 233 service3 := makeDeployment(env, "not"+prefix+"service3") 234 235 requests := patchInstancesResponses(c, prefix, service1, service2, service3) 236 instances, err := env.AllInstances() 237 c.Assert(err, gc.IsNil) 238 c.Check(len(instances), gc.Equals, 3) 239 c.Check(instances[0].Id(), gc.Equals, instance.Id(prefix+"service1")) 240 service2Role1Name := service2.Deployments[0].RoleList[0].RoleName 241 service2Role2Name := service2.Deployments[0].RoleList[1].RoleName 242 c.Check(instances[1].Id(), gc.Equals, instance.Id(prefix+"service2-"+service2Role1Name)) 243 c.Check(instances[2].Id(), gc.Equals, instance.Id(prefix+"service2-"+service2Role2Name)) 244 c.Check(len(*requests), gc.Equals, 3) 245 } 246 247 func (suite *environSuite) TestInstancesReturnsFilteredList(c *gc.C) { 248 env := makeEnviron(c) 249 prefix := env.getEnvPrefix() 250 service := makeDeployment(env, prefix+"service") 251 requests := patchInstancesResponses(c, prefix, service) 252 role1Name := service.Deployments[0].RoleList[0].RoleName 253 instId := instance.Id(prefix + "service-" + role1Name) 254 instances, err := env.Instances([]instance.Id{instId}) 255 c.Assert(err, gc.IsNil) 256 c.Check(len(instances), gc.Equals, 1) 257 c.Check(instances[0].Id(), gc.Equals, instId) 258 c.Check(len(*requests), gc.Equals, 2) 259 } 260 261 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNoInstancesRequested(c *gc.C) { 262 services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-1"}, {ServiceName: "deployment-2"}} 263 patchWithServiceListResponse(c, services) 264 env := makeEnviron(c) 265 instances, err := env.Instances([]instance.Id{}) 266 c.Check(err, gc.Equals, environs.ErrNoInstances) 267 c.Check(instances, gc.IsNil) 268 } 269 270 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNoInstanceFound(c *gc.C) { 271 services := []gwacl.HostedServiceDescriptor{} 272 patchWithServiceListResponse(c, services) 273 env := makeEnviron(c) 274 instances, err := env.Instances([]instance.Id{"deploy-id"}) 275 c.Check(err, gc.Equals, environs.ErrNoInstances) 276 c.Check(instances, gc.IsNil) 277 } 278 279 func (suite *environSuite) TestInstancesReturnsPartialInstancesIfSomeInstancesAreNotFound(c *gc.C) { 280 env := makeEnviron(c) 281 prefix := env.getEnvPrefix() 282 service := makeDeployment(env, prefix+"service") 283 284 role1Name := service.Deployments[0].RoleList[0].RoleName 285 role2Name := service.Deployments[0].RoleList[1].RoleName 286 inst1Id := instance.Id(prefix + "service-" + role1Name) 287 inst2Id := instance.Id(prefix + "service-" + role2Name) 288 patchInstancesResponses(c, prefix, service) 289 290 instances, err := env.Instances([]instance.Id{inst1Id, "unknown", inst2Id}) 291 c.Assert(err, gc.Equals, environs.ErrPartialInstances) 292 c.Check(len(instances), gc.Equals, 3) 293 c.Check(instances[0].Id(), gc.Equals, inst1Id) 294 c.Check(instances[1], gc.IsNil) 295 c.Check(instances[2].Id(), gc.Equals, inst2Id) 296 } 297 298 func (*environSuite) TestStorage(c *gc.C) { 299 env := makeEnviron(c) 300 baseStorage := env.Storage() 301 storage, ok := baseStorage.(*azureStorage) 302 c.Check(ok, gc.Equals, true) 303 c.Assert(storage, gc.NotNil) 304 c.Check(storage.storageContext.getContainer(), gc.Equals, env.getContainerName()) 305 context, err := storage.getStorageContext() 306 c.Assert(err, gc.IsNil) 307 c.Check(context.Account, gc.Equals, env.ecfg.storageAccountName()) 308 c.Check(context.RetryPolicy, gc.DeepEquals, retryPolicy) 309 } 310 311 func (*environSuite) TestQueryStorageAccountKeyGetsKey(c *gc.C) { 312 env := makeEnviron(c) 313 keysInAzure := gwacl.StorageAccountKeys{Primary: "a-key"} 314 azureResponse, err := xml.Marshal(keysInAzure) 315 c.Assert(err, gc.IsNil) 316 requests := gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 317 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 318 }) 319 320 returnedKey, err := env.queryStorageAccountKey() 321 c.Assert(err, gc.IsNil) 322 323 c.Check(returnedKey, gc.Equals, keysInAzure.Primary) 324 c.Assert(*requests, gc.HasLen, 1) 325 c.Check((*requests)[0].Method, gc.Equals, "GET") 326 } 327 328 func (*environSuite) TestGetStorageContextCreatesStorageContext(c *gc.C) { 329 env := makeEnviron(c) 330 stor, err := env.getStorageContext() 331 c.Assert(err, gc.IsNil) 332 c.Assert(stor, gc.NotNil) 333 c.Check(stor.Account, gc.Equals, env.ecfg.storageAccountName()) 334 c.Check(stor.AzureEndpoint, gc.Equals, gwacl.GetEndpoint(env.ecfg.location())) 335 } 336 337 func (*environSuite) TestGetStorageContextUsesKnownStorageAccountKey(c *gc.C) { 338 env := makeEnviron(c) 339 env.storageAccountKey = "my-key" 340 341 stor, err := env.getStorageContext() 342 c.Assert(err, gc.IsNil) 343 344 c.Check(stor.Key, gc.Equals, "my-key") 345 } 346 347 func (*environSuite) TestGetStorageContextQueriesStorageAccountKeyIfNeeded(c *gc.C) { 348 env := makeEnviron(c) 349 env.storageAccountKey = "" 350 keysInAzure := gwacl.StorageAccountKeys{Primary: "my-key"} 351 azureResponse, err := xml.Marshal(keysInAzure) 352 c.Assert(err, gc.IsNil) 353 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 354 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 355 }) 356 357 stor, err := env.getStorageContext() 358 c.Assert(err, gc.IsNil) 359 360 c.Check(stor.Key, gc.Equals, keysInAzure.Primary) 361 c.Check(env.storageAccountKey, gc.Equals, keysInAzure.Primary) 362 } 363 364 func (*environSuite) TestGetStorageContextFailsIfNoKeyAvailable(c *gc.C) { 365 env := makeEnviron(c) 366 env.storageAccountKey = "" 367 azureResponse, err := xml.Marshal(gwacl.StorageAccountKeys{}) 368 c.Assert(err, gc.IsNil) 369 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 370 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 371 }) 372 373 _, err = env.getStorageContext() 374 c.Assert(err, gc.NotNil) 375 376 c.Check(err, gc.ErrorMatches, "no keys available for storage account") 377 } 378 379 func (*environSuite) TestUpdateStorageAccountKeyGetsFreshKey(c *gc.C) { 380 env := makeEnviron(c) 381 keysInAzure := gwacl.StorageAccountKeys{Primary: "my-key"} 382 azureResponse, err := xml.Marshal(keysInAzure) 383 c.Assert(err, gc.IsNil) 384 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 385 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 386 }) 387 388 key, err := env.updateStorageAccountKey(env.getSnapshot()) 389 c.Assert(err, gc.IsNil) 390 391 c.Check(key, gc.Equals, keysInAzure.Primary) 392 c.Check(env.storageAccountKey, gc.Equals, keysInAzure.Primary) 393 } 394 395 func (*environSuite) TestUpdateStorageAccountKeyReturnsError(c *gc.C) { 396 env := makeEnviron(c) 397 env.storageAccountKey = "" 398 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 399 gwacl.NewDispatcherResponse(nil, http.StatusInternalServerError, nil), 400 }) 401 402 _, err := env.updateStorageAccountKey(env.getSnapshot()) 403 c.Assert(err, gc.NotNil) 404 405 c.Check(err, gc.ErrorMatches, "cannot obtain storage account keys: GET request failed.*Internal Server Error.*") 406 c.Check(env.storageAccountKey, gc.Equals, "") 407 } 408 409 func (*environSuite) TestUpdateStorageAccountKeyDetectsConcurrentUpdate(c *gc.C) { 410 env := makeEnviron(c) 411 env.storageAccountKey = "" 412 keysInAzure := gwacl.StorageAccountKeys{Primary: "my-key"} 413 azureResponse, err := xml.Marshal(keysInAzure) 414 c.Assert(err, gc.IsNil) 415 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 416 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 417 }) 418 419 // Here we use a snapshot that's different from the environment, to 420 // simulate a concurrent change to the environment. 421 _, err = env.updateStorageAccountKey(makeEnviron(c)) 422 c.Assert(err, gc.NotNil) 423 424 // updateStorageAccountKey detects the change, and refuses to write its 425 // outdated information into env. 426 c.Check(err, gc.ErrorMatches, "environment was reconfigured") 427 c.Check(env.storageAccountKey, gc.Equals, "") 428 } 429 430 func (*environSuite) TestSetConfigValidates(c *gc.C) { 431 env := makeEnviron(c) 432 originalCfg := env.ecfg 433 attrs := makeAzureConfigMap(c) 434 // This config is not valid. It lacks essential information. 435 delete(attrs, "management-subscription-id") 436 badCfg, err := config.New(config.NoDefaults, attrs) 437 c.Assert(err, gc.IsNil) 438 439 err = env.SetConfig(badCfg) 440 441 // Since the config was not valid, SetConfig returns an error. It 442 // does not update the environment's config either. 443 c.Check(err, gc.NotNil) 444 c.Check( 445 err, 446 gc.ErrorMatches, 447 "management-subscription-id: expected string, got nothing") 448 c.Check(env.ecfg, gc.Equals, originalCfg) 449 } 450 451 func (*environSuite) TestSetConfigUpdatesConfig(c *gc.C) { 452 env := makeEnviron(c) 453 // We're going to set a new config. It can be recognized by its 454 // unusual default Ubuntu release series: 7.04 Feisty Fawn. 455 attrs := makeAzureConfigMap(c) 456 attrs["default-series"] = "feisty" 457 cfg, err := config.New(config.NoDefaults, attrs) 458 c.Assert(err, gc.IsNil) 459 460 err = env.SetConfig(cfg) 461 c.Assert(err, gc.IsNil) 462 463 c.Check(config.PreferredSeries(env.ecfg.Config), gc.Equals, "feisty") 464 } 465 466 func (*environSuite) TestSetConfigLocksEnviron(c *gc.C) { 467 env := makeEnviron(c) 468 cfg, err := config.New(config.NoDefaults, makeAzureConfigMap(c)) 469 c.Assert(err, gc.IsNil) 470 471 coretesting.TestLockingFunction(&env.Mutex, func() { env.SetConfig(cfg) }) 472 } 473 474 func (*environSuite) TestSetConfigWillNotUpdateName(c *gc.C) { 475 // Once the environment's name has been set, it cannot be updated. 476 // Global validation rejects such a change. 477 // This matters because the attribute is not protected by a lock. 478 env := makeEnviron(c) 479 originalName := env.Name() 480 attrs := makeAzureConfigMap(c) 481 attrs["name"] = "new-name" 482 cfg, err := config.New(config.NoDefaults, attrs) 483 c.Assert(err, gc.IsNil) 484 485 err = env.SetConfig(cfg) 486 487 c.Assert(err, gc.NotNil) 488 c.Check( 489 err, 490 gc.ErrorMatches, 491 `cannot change name from ".*" to "new-name"`) 492 c.Check(env.Name(), gc.Equals, originalName) 493 } 494 495 func (*environSuite) TestSetConfigClearsStorageAccountKey(c *gc.C) { 496 env := makeEnviron(c) 497 env.storageAccountKey = "key-for-previous-config" 498 attrs := makeAzureConfigMap(c) 499 attrs["default-series"] = "other" 500 cfg, err := config.New(config.NoDefaults, attrs) 501 c.Assert(err, gc.IsNil) 502 503 err = env.SetConfig(cfg) 504 c.Assert(err, gc.IsNil) 505 506 c.Check(env.storageAccountKey, gc.Equals, "") 507 } 508 509 func (s *environSuite) TestStateInfoFailsIfNoStateInstances(c *gc.C) { 510 env := makeEnviron(c) 511 s.setDummyStorage(c, env) 512 _, _, err := env.StateInfo() 513 c.Check(err, gc.Equals, environs.ErrNotBootstrapped) 514 } 515 516 func (s *environSuite) TestStateInfo(c *gc.C) { 517 env := makeEnviron(c) 518 s.setDummyStorage(c, env) 519 prefix := env.getEnvPrefix() 520 521 service := makeDeployment(env, prefix+"myservice") 522 instId := instance.Id(service.ServiceName + "-" + service.Deployments[0].RoleList[0].RoleName) 523 patchInstancesResponses(c, prefix, service) 524 err := bootstrap.SaveState( 525 env.Storage(), 526 &bootstrap.BootstrapState{StateInstances: []instance.Id{instId}}, 527 ) 528 c.Assert(err, gc.IsNil) 529 530 responses := prepareInstancesResponses(c, prefix, service) 531 responses = append(responses, prepareDeploymentInfoResponse(c, gwacl.Deployment{ 532 RoleInstanceList: []gwacl.RoleInstance{{ 533 RoleName: service.Deployments[0].RoleList[0].RoleName, 534 IPAddress: "1.2.3.4", 535 }}, 536 VirtualNetworkName: env.getVirtualNetworkName(), 537 })...) 538 gwacl.PatchManagementAPIResponses(responses) 539 540 stateInfo, apiInfo, err := env.StateInfo() 541 c.Assert(err, gc.IsNil) 542 config := env.Config() 543 dnsName := prefix + "myservice." + AzureDomainName 544 c.Check(stateInfo.Addrs, jc.SameContents, []string{ 545 fmt.Sprintf("1.2.3.4:%d", config.StatePort()), 546 fmt.Sprintf("%s:%d", dnsName, config.StatePort()), 547 }) 548 c.Check(apiInfo.Addrs, jc.DeepEquals, []string{ 549 fmt.Sprintf("1.2.3.4:%d", config.APIPort()), 550 fmt.Sprintf("%s:%d", dnsName, config.APIPort()), 551 }) 552 } 553 554 // parseCreateServiceRequest reconstructs the original CreateHostedService 555 // request object passed to gwacl's AddHostedService method, based on the 556 // X509Request which the method issues. 557 func parseCreateServiceRequest(c *gc.C, request *gwacl.X509Request) *gwacl.CreateHostedService { 558 body := gwacl.CreateHostedService{} 559 err := xml.Unmarshal(request.Payload, &body) 560 c.Assert(err, gc.IsNil) 561 return &body 562 } 563 564 // getHostedServicePropertiesServiceName extracts the service name parameter 565 // from the GetHostedServiceProperties request URL. 566 func getHostedServicePropertiesServiceName(c *gc.C, request *gwacl.X509Request) string { 567 url, err := url.Parse(request.URL) 568 c.Assert(err, gc.IsNil) 569 return path.Base(url.Path) 570 } 571 572 // makeNonAvailabilityResponse simulates a reply to the 573 // CheckHostedServiceNameAvailability call saying that a name is not available. 574 func makeNonAvailabilityResponse(c *gc.C) []byte { 575 errorBody, err := xml.Marshal(gwacl.AvailabilityResponse{ 576 Result: "false", 577 Reason: "he's a very naughty boy"}) 578 c.Assert(err, gc.IsNil) 579 return errorBody 580 } 581 582 // makeAvailabilityResponse simulates a reply to the 583 // CheckHostedServiceNameAvailability call saying that a name is available. 584 func makeAvailabilityResponse(c *gc.C) []byte { 585 errorBody, err := xml.Marshal(gwacl.AvailabilityResponse{ 586 Result: "true"}) 587 c.Assert(err, gc.IsNil) 588 return errorBody 589 } 590 591 func (*environSuite) TestAttemptCreateServiceCreatesService(c *gc.C) { 592 prefix := "myservice" 593 affinityGroup := "affinity-group" 594 595 responses := []gwacl.DispatcherResponse{ 596 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil), 597 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 598 } 599 requests := gwacl.PatchManagementAPIResponses(responses) 600 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 601 c.Assert(err, gc.IsNil) 602 603 service, err := attemptCreateService(azure, prefix, affinityGroup, "") 604 c.Assert(err, gc.IsNil) 605 606 c.Assert(*requests, gc.HasLen, 2) 607 body := parseCreateServiceRequest(c, (*requests)[1]) 608 c.Check(body.ServiceName, gc.Equals, service.ServiceName) 609 c.Check(body.AffinityGroup, gc.Equals, affinityGroup) 610 c.Check(service.ServiceName, gc.Matches, prefix+".*") 611 // We specify AffinityGroup, so Location should be empty. 612 c.Check(service.Location, gc.Equals, "") 613 } 614 615 func (*environSuite) TestAttemptCreateServiceReturnsNilIfNameNotUnique(c *gc.C) { 616 responses := []gwacl.DispatcherResponse{ 617 gwacl.NewDispatcherResponse(makeNonAvailabilityResponse(c), http.StatusOK, nil), 618 } 619 gwacl.PatchManagementAPIResponses(responses) 620 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 621 c.Assert(err, gc.IsNil) 622 623 service, err := attemptCreateService(azure, "service", "affinity-group", "") 624 c.Check(err, gc.IsNil) 625 c.Check(service, gc.IsNil) 626 } 627 628 func (*environSuite) TestAttemptCreateServicePropagatesOtherFailure(c *gc.C) { 629 responses := []gwacl.DispatcherResponse{ 630 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil), 631 gwacl.NewDispatcherResponse(nil, http.StatusNotFound, nil), 632 } 633 gwacl.PatchManagementAPIResponses(responses) 634 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 635 c.Assert(err, gc.IsNil) 636 637 _, err = attemptCreateService(azure, "service", "affinity-group", "") 638 c.Assert(err, gc.NotNil) 639 c.Check(err, gc.ErrorMatches, ".*Not Found.*") 640 } 641 642 func (*environSuite) TestNewHostedServiceCreatesService(c *gc.C) { 643 prefix := "myservice" 644 affinityGroup := "affinity-group" 645 responses := []gwacl.DispatcherResponse{ 646 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil), 647 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 648 getAzureServiceResponse(c, gwacl.HostedService{ 649 HostedServiceDescriptor: gwacl.HostedServiceDescriptor{ 650 ServiceName: "anything", 651 }, 652 }), 653 } 654 requests := gwacl.PatchManagementAPIResponses(responses) 655 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 656 c.Assert(err, gc.IsNil) 657 658 service, err := newHostedService(azure, prefix, affinityGroup, "") 659 c.Assert(err, gc.IsNil) 660 661 c.Assert(*requests, gc.HasLen, 3) 662 body := parseCreateServiceRequest(c, (*requests)[1]) 663 requestedServiceName := getHostedServicePropertiesServiceName(c, (*requests)[2]) 664 c.Check(body.ServiceName, gc.Matches, prefix+".*") 665 c.Check(body.ServiceName, gc.Equals, requestedServiceName) 666 c.Check(body.AffinityGroup, gc.Equals, affinityGroup) 667 c.Check(service.ServiceName, gc.Equals, "anything") 668 c.Check(service.Location, gc.Equals, "") 669 } 670 671 func (*environSuite) TestNewHostedServiceRetriesIfNotUnique(c *gc.C) { 672 errorBody := makeNonAvailabilityResponse(c) 673 okBody := makeAvailabilityResponse(c) 674 // In this scenario, the first two names that we try are already 675 // taken. The third one is unique though, so we succeed. 676 responses := []gwacl.DispatcherResponse{ 677 gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil), 678 gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil), 679 gwacl.NewDispatcherResponse(okBody, http.StatusOK, nil), // name is unique 680 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), // create service 681 getAzureServiceResponse(c, gwacl.HostedService{ 682 HostedServiceDescriptor: gwacl.HostedServiceDescriptor{ 683 ServiceName: "anything", 684 }, 685 }), 686 } 687 requests := gwacl.PatchManagementAPIResponses(responses) 688 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 689 c.Assert(err, gc.IsNil) 690 691 service, err := newHostedService(azure, "service", "affinity-group", "") 692 c.Check(err, gc.IsNil) 693 694 c.Assert(*requests, gc.HasLen, 5) 695 // How many names have been attempted, and how often? 696 // There is a minute chance that this tries the same name twice, and 697 // then this test will fail. If that happens, try seeding the 698 // randomizer with some fixed seed that doens't produce the problem. 699 attemptedNames := make(map[string]int) 700 for _, request := range *requests { 701 // Exit the loop if we hit the request to create the service, it comes 702 // after the check calls. 703 if request.Method == "POST" { 704 break 705 } 706 // Name is the last part of the URL from the GET requests that check 707 // availability. 708 _, name := path.Split(strings.TrimRight(request.URL, "/")) 709 attemptedNames[name] += 1 710 } 711 // The three attempts we just made all had different service names. 712 c.Check(attemptedNames, gc.HasLen, 3) 713 714 // Once newHostedService succeeds, we get a hosted service with the 715 // name returned from GetHostedServiceProperties. 716 c.Check(service.ServiceName, gc.Equals, "anything") 717 } 718 719 func (*environSuite) TestNewHostedServiceFailsIfUnableToFindUniqueName(c *gc.C) { 720 errorBody := makeNonAvailabilityResponse(c) 721 responses := []gwacl.DispatcherResponse{} 722 for counter := 0; counter < 100; counter++ { 723 responses = append(responses, gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil)) 724 } 725 gwacl.PatchManagementAPIResponses(responses) 726 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 727 c.Assert(err, gc.IsNil) 728 729 _, err = newHostedService(azure, "service", "affinity-group", "") 730 c.Assert(err, gc.NotNil) 731 c.Check(err, gc.ErrorMatches, "could not come up with a unique hosted service name.*") 732 } 733 734 func buildGetServicePropertiesResponses(c *gc.C, services ...*gwacl.HostedService) []gwacl.DispatcherResponse { 735 responses := make([]gwacl.DispatcherResponse, len(services)) 736 for i, service := range services { 737 serviceXML, err := service.Serialize() 738 c.Assert(err, gc.IsNil) 739 responses[i] = gwacl.NewDispatcherResponse([]byte(serviceXML), http.StatusOK, nil) 740 } 741 return responses 742 } 743 744 func buildStatusOKResponses(c *gc.C, n int) []gwacl.DispatcherResponse { 745 responses := make([]gwacl.DispatcherResponse, n) 746 for i := range responses { 747 responses[i] = gwacl.NewDispatcherResponse(nil, http.StatusOK, nil) 748 } 749 return responses 750 } 751 752 func makeAzureService(name string) *gwacl.HostedService { 753 return &gwacl.HostedService{ 754 HostedServiceDescriptor: gwacl.HostedServiceDescriptor{ServiceName: name}, 755 } 756 } 757 758 func makeRole(env *azureEnviron) *gwacl.Role { 759 size := "Large" 760 vhd := env.newOSDisk("source-image-name") 761 userData := "example-user-data" 762 return env.newRole(size, vhd, userData, false) 763 } 764 765 func makeLegacyDeployment(env *azureEnviron, serviceName string) *gwacl.HostedService { 766 service := makeAzureService(serviceName) 767 service.Deployments = []gwacl.Deployment{{ 768 Name: serviceName, 769 RoleList: []gwacl.Role{*makeRole(env)}, 770 }} 771 return service 772 } 773 774 func makeDeployment(env *azureEnviron, serviceName string) *gwacl.HostedService { 775 service := makeAzureService(serviceName) 776 service.Deployments = []gwacl.Deployment{{ 777 Name: serviceName + "-v2", 778 RoleList: []gwacl.Role{*makeRole(env), *makeRole(env)}, 779 }} 780 return service 781 } 782 783 func (s *environSuite) TestStopInstancesDestroysMachines(c *gc.C) { 784 env := makeEnviron(c) 785 prefix := env.getEnvPrefix() 786 service1Name := "service1" 787 service1 := makeLegacyDeployment(env, prefix+service1Name) 788 service2Name := "service2" 789 service2 := makeDeployment(env, prefix+service2Name) 790 791 inst1, err := env.getInstance(service1, "") 792 c.Assert(err, gc.IsNil) 793 role2Name := service2.Deployments[0].RoleList[0].RoleName 794 inst2, err := env.getInstance(service2, role2Name) 795 c.Assert(err, gc.IsNil) 796 role3Name := service2.Deployments[0].RoleList[1].RoleName 797 inst3, err := env.getInstance(service2, role3Name) 798 c.Assert(err, gc.IsNil) 799 800 responses := buildGetServicePropertiesResponses(c, service1) 801 responses = append(responses, buildStatusOKResponses(c, 1)...) // DeleteHostedService 802 responses = append(responses, buildGetServicePropertiesResponses(c, service2)...) 803 responses = append(responses, buildStatusOKResponses(c, 1)...) // DeleteHostedService 804 requests := gwacl.PatchManagementAPIResponses(responses) 805 err = env.StopInstances(inst1.Id(), inst2.Id(), inst3.Id()) 806 c.Check(err, gc.IsNil) 807 808 // One GET and DELETE per service 809 // (GetHostedServiceProperties and DeleteHostedService). 810 c.Check(len(*requests), gc.Equals, len(responses)) 811 assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".") 812 assertOneRequestMatches(c, *requests, "GET", ".*"+service2Name+".*") 813 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1Name+".*") 814 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service2Name+".*") 815 } 816 817 func (s *environSuite) TestStopInstancesServiceSubset(c *gc.C) { 818 env := makeEnviron(c) 819 service := makeDeployment(env, env.getEnvPrefix()+"service") 820 821 role1Name := service.Deployments[0].RoleList[0].RoleName 822 inst1, err := env.getInstance(service, role1Name) 823 c.Assert(err, gc.IsNil) 824 825 responses := buildGetServicePropertiesResponses(c, service) 826 responses = append(responses, buildStatusOKResponses(c, 1)...) // DeleteRole 827 requests := gwacl.PatchManagementAPIResponses(responses) 828 err = env.StopInstances(inst1.Id()) 829 c.Check(err, gc.IsNil) 830 831 // One GET for the service, and one DELETE for the role. 832 // The service isn't deleted because it has two roles, 833 // and only one is being deleted. 834 c.Check(len(*requests), gc.Equals, len(responses)) 835 assertOneRequestMatches(c, *requests, "GET", ".*"+service.ServiceName+".") 836 assertOneRequestMatches(c, *requests, "DELETE", ".*"+role1Name+".*") 837 } 838 839 func (s *environSuite) TestStopInstancesWhenStoppingMachinesFails(c *gc.C) { 840 env := makeEnviron(c) 841 prefix := env.getEnvPrefix() 842 service1 := makeDeployment(env, prefix+"service1") 843 service2 := makeDeployment(env, prefix+"service2") 844 service1Role1Name := service1.Deployments[0].RoleList[0].RoleName 845 inst1, err := env.getInstance(service1, service1Role1Name) 846 c.Assert(err, gc.IsNil) 847 service2Role1Name := service2.Deployments[0].RoleList[0].RoleName 848 inst2, err := env.getInstance(service2, service2Role1Name) 849 c.Assert(err, gc.IsNil) 850 851 responses := buildGetServicePropertiesResponses(c, service1) 852 // Failed to delete one of the services. This will cause StopInstances to stop 853 // immediately. 854 responses = append(responses, gwacl.NewDispatcherResponse(nil, http.StatusConflict, nil)) 855 requests := gwacl.PatchManagementAPIResponses(responses) 856 857 err = env.StopInstances(inst1.Id(), inst2.Id()) 858 c.Check(err, gc.ErrorMatches, ".*Conflict.*") 859 860 c.Check(len(*requests), gc.Equals, len(responses)) 861 assertOneRequestMatches(c, *requests, "GET", ".*"+service1.ServiceName+".*") 862 assertOneRequestMatches(c, *requests, "DELETE", service1.ServiceName) 863 } 864 865 func (s *environSuite) TestStopInstancesWithZeroInstance(c *gc.C) { 866 env := makeEnviron(c) 867 err := env.StopInstances() 868 c.Check(err, gc.IsNil) 869 } 870 871 // getVnetAndAffinityGroupCleanupResponses returns the responses 872 // (gwacl.DispatcherResponse) that a fake http server should return 873 // when gwacl's RemoveVirtualNetworkSite() and DeleteAffinityGroup() 874 // are called. 875 func getVnetAndAffinityGroupCleanupResponses(c *gc.C) []gwacl.DispatcherResponse { 876 existingConfig := &gwacl.NetworkConfiguration{ 877 XMLNS: gwacl.XMLNS_NC, 878 VirtualNetworkSites: nil, 879 } 880 body, err := existingConfig.Serialize() 881 c.Assert(err, gc.IsNil) 882 cleanupResponses := []gwacl.DispatcherResponse{ 883 // Return empty net configuration. 884 gwacl.NewDispatcherResponse([]byte(body), http.StatusOK, nil), 885 // Accept deletion of affinity group. 886 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 887 } 888 return cleanupResponses 889 } 890 891 func (s *environSuite) TestDestroyDoesNotCleanStorageIfError(c *gc.C) { 892 env := makeEnviron(c) 893 s.setDummyStorage(c, env) 894 // Populate storage. 895 err := bootstrap.SaveState( 896 env.Storage(), 897 &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id("test-id")}}) 898 c.Assert(err, gc.IsNil) 899 responses := []gwacl.DispatcherResponse{ 900 gwacl.NewDispatcherResponse(nil, http.StatusBadRequest, nil), 901 } 902 gwacl.PatchManagementAPIResponses(responses) 903 904 err = env.Destroy() 905 c.Check(err, gc.NotNil) 906 907 files, err := storage.List(env.Storage(), "") 908 c.Assert(err, gc.IsNil) 909 c.Check(files, gc.HasLen, 1) 910 } 911 912 func (s *environSuite) TestDestroyCleansUpStorage(c *gc.C) { 913 env := makeEnviron(c) 914 s.setDummyStorage(c, env) 915 // Populate storage. 916 err := bootstrap.SaveState( 917 env.Storage(), 918 &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id("test-id")}}) 919 c.Assert(err, gc.IsNil) 920 responses := getAzureServiceListResponse(c) 921 cleanupResponses := getVnetAndAffinityGroupCleanupResponses(c) 922 responses = append(responses, cleanupResponses...) 923 gwacl.PatchManagementAPIResponses(responses) 924 925 err = env.Destroy() 926 c.Check(err, gc.IsNil) 927 928 files, err := storage.List(env.Storage(), "") 929 c.Assert(err, gc.IsNil) 930 c.Check(files, gc.HasLen, 0) 931 } 932 933 func (s *environSuite) TestDestroyDeletesVirtualNetworkAndAffinityGroup(c *gc.C) { 934 env := makeEnviron(c) 935 s.setDummyStorage(c, env) 936 responses := getAzureServiceListResponse(c) 937 // Prepare a configuration with a single virtual network. 938 existingConfig := &gwacl.NetworkConfiguration{ 939 XMLNS: gwacl.XMLNS_NC, 940 VirtualNetworkSites: &[]gwacl.VirtualNetworkSite{ 941 {Name: env.getVirtualNetworkName()}, 942 }, 943 } 944 body, err := existingConfig.Serialize() 945 c.Assert(err, gc.IsNil) 946 cleanupResponses := []gwacl.DispatcherResponse{ 947 // Return existing configuration. 948 gwacl.NewDispatcherResponse([]byte(body), http.StatusOK, nil), 949 // Accept upload of new configuration. 950 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 951 // Accept deletion of affinity group. 952 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 953 } 954 responses = append(responses, cleanupResponses...) 955 requests := gwacl.PatchManagementAPIResponses(responses) 956 957 err = env.Destroy() 958 c.Check(err, gc.IsNil) 959 960 c.Assert(*requests, gc.HasLen, 4) 961 // One request to get the network configuration. 962 getRequest := (*requests)[1] 963 c.Check(getRequest.Method, gc.Equals, "GET") 964 c.Check(strings.HasSuffix(getRequest.URL, "services/networking/media"), gc.Equals, true) 965 // One request to upload the new version of the network configuration. 966 putRequest := (*requests)[2] 967 c.Check(putRequest.Method, gc.Equals, "PUT") 968 c.Check(strings.HasSuffix(putRequest.URL, "services/networking/media"), gc.Equals, true) 969 // One request to delete the Affinity Group. 970 agRequest := (*requests)[3] 971 c.Check(strings.Contains(agRequest.URL, env.getAffinityGroupName()), jc.IsTrue) 972 c.Check(agRequest.Method, gc.Equals, "DELETE") 973 974 } 975 976 var emptyListResponse = ` 977 <?xml version="1.0" encoding="utf-8"?> 978 <EnumerationResults ContainerName="http://myaccount.blob.core.windows.net/mycontainer"> 979 <Prefix>prefix</Prefix> 980 <Marker>marker</Marker> 981 <MaxResults>maxresults</MaxResults> 982 <Delimiter>delimiter</Delimiter> 983 <Blobs></Blobs> 984 <NextMarker /> 985 </EnumerationResults>` 986 987 // assertOneRequestMatches asserts that at least one request in the given slice 988 // contains a request with the given method and whose URL matches the given regexp. 989 func assertOneRequestMatches(c *gc.C, requests []*gwacl.X509Request, method string, urlPattern string) { 990 for _, request := range requests { 991 matched, err := regexp.MatchString(urlPattern, request.URL) 992 if err == nil && request.Method == method && matched { 993 return 994 } 995 } 996 c.Error(fmt.Sprintf("none of the requests matches: Method=%v, URL pattern=%v", method, urlPattern)) 997 } 998 999 func (s *environSuite) TestDestroyStopsAllInstances(c *gc.C) { 1000 env := makeEnviron(c) 1001 s.setDummyStorage(c, env) 1002 prefix := env.getEnvPrefix() 1003 service1 := makeDeployment(env, prefix+"service1") 1004 service2 := makeDeployment(env, prefix+"service2") 1005 1006 // The call to AllInstances() will return only one service (service1). 1007 responses := getAzureServiceListResponse(c, service1.HostedServiceDescriptor, service2.HostedServiceDescriptor) 1008 responses = append(responses, buildStatusOKResponses(c, 2)...) // DeleteHostedService 1009 responses = append(responses, getVnetAndAffinityGroupCleanupResponses(c)...) 1010 requests := gwacl.PatchManagementAPIResponses(responses) 1011 1012 err := env.Destroy() 1013 c.Check(err, gc.IsNil) 1014 1015 // One request to get the list of all the environment's instances. 1016 // One delete request per destroyed service, and two additional 1017 // requests to delete the Virtual Network and the Affinity Group. 1018 c.Check((*requests), gc.HasLen, 5) 1019 c.Check((*requests)[0].Method, gc.Equals, "GET") 1020 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1.ServiceName+".*") 1021 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service2.ServiceName+".*") 1022 } 1023 1024 func (s *environSuite) TestGetInstance(c *gc.C) { 1025 env := makeEnviron(c) 1026 service1 := makeLegacyDeployment(env, "service1") 1027 service2 := makeDeployment(env, "service1") 1028 1029 // azureEnviron.Instances will call getInstance with roleName=="" 1030 // for legacy instances. This will cause getInstance to get the 1031 // one and only role (or error if there is more than one). 1032 inst1, err := env.getInstance(service1, "") 1033 c.Assert(err, gc.IsNil) 1034 c.Check(inst1.Id(), gc.Equals, instance.Id("service1")) 1035 c.Assert(inst1, gc.FitsTypeOf, &azureInstance{}) 1036 c.Check(inst1.(*azureInstance).environ, gc.Equals, env) 1037 c.Check(inst1.(*azureInstance).roleName, gc.Equals, service1.Deployments[0].RoleList[0].RoleName) 1038 service1.Deployments[0].RoleList = service2.Deployments[0].RoleList 1039 inst1, err = env.getInstance(service1, "") 1040 c.Check(err, gc.ErrorMatches, `expected one role for "service1", got 2`) 1041 1042 inst2, err := env.getInstance(service2, service2.Deployments[0].RoleList[0].RoleName) 1043 c.Assert(err, gc.IsNil) 1044 c.Check(inst2.Id(), gc.Equals, instance.Id("service1-"+service2.Deployments[0].RoleList[0].RoleName)) 1045 } 1046 1047 func (s *environSuite) TestInitialPorts(c *gc.C) { 1048 env := makeEnviron(c) 1049 service1 := makeLegacyDeployment(env, "service1") 1050 service2 := makeDeployment(env, "service2") 1051 service3 := makeDeployment(env, "service3") 1052 service3.Label = base64.StdEncoding.EncodeToString([]byte(stateServerLabel)) 1053 1054 role1 := &service1.Deployments[0].RoleList[0] 1055 inst1, err := env.getInstance(service1, role1.RoleName) 1056 c.Assert(err, gc.IsNil) 1057 c.Assert(inst1.(*azureInstance).maskStateServerPorts, jc.IsTrue) 1058 role2 := &service2.Deployments[0].RoleList[0] 1059 inst2, err := env.getInstance(service2, role2.RoleName) 1060 c.Assert(err, gc.IsNil) 1061 role3 := &service3.Deployments[0].RoleList[0] 1062 inst3, err := env.getInstance(service3, role3.RoleName) 1063 c.Assert(err, gc.IsNil) 1064 1065 // Only role2 should report opened state server ports via the Ports method. 1066 dummyRole := *role1 1067 configSetNetwork(&dummyRole).InputEndpoints = &[]gwacl.InputEndpoint{{ 1068 LocalPort: env.Config().StatePort(), 1069 Protocol: "tcp", 1070 Name: "stateserver", 1071 Port: env.Config().StatePort(), 1072 }, { 1073 LocalPort: env.Config().APIPort(), 1074 Protocol: "tcp", 1075 Name: "apiserver", 1076 Port: env.Config().APIPort(), 1077 }} 1078 reportsStateServerPorts := func(inst instance.Instance) bool { 1079 responses := preparePortChangeConversation(c, &dummyRole) 1080 gwacl.PatchManagementAPIResponses(responses) 1081 ports, err := inst.Ports("") 1082 c.Assert(err, gc.IsNil) 1083 portmap := make(map[int]bool) 1084 for _, port := range ports { 1085 portmap[port.Number] = true 1086 } 1087 return portmap[env.Config().StatePort()] && portmap[env.Config().APIPort()] 1088 } 1089 c.Check(inst1, gc.Not(jc.Satisfies), reportsStateServerPorts) 1090 c.Check(inst2, jc.Satisfies, reportsStateServerPorts) 1091 c.Check(inst3, gc.Not(jc.Satisfies), reportsStateServerPorts) 1092 } 1093 1094 func (*environSuite) TestNewOSVirtualDisk(c *gc.C) { 1095 env := makeEnviron(c) 1096 sourceImageName := "source-image-name" 1097 1098 vhd := env.newOSDisk(sourceImageName) 1099 1100 mediaLinkUrl, err := url.Parse(vhd.MediaLink) 1101 c.Check(err, gc.IsNil) 1102 storageAccount := env.ecfg.storageAccountName() 1103 c.Check(mediaLinkUrl.Host, gc.Equals, fmt.Sprintf("%s.blob.core.windows.net", storageAccount)) 1104 c.Check(vhd.SourceImageName, gc.Equals, sourceImageName) 1105 } 1106 1107 // mapInputEndpointsByPort takes a slice of input endpoints, and returns them 1108 // as a map keyed by their (external) ports. This makes it easier to query 1109 // individual endpoints from an array whose ordering you don't know. 1110 // Multiple input endpoints for the same port are treated as an error. 1111 func mapInputEndpointsByPort(c *gc.C, endpoints []gwacl.InputEndpoint) map[int]gwacl.InputEndpoint { 1112 mapping := make(map[int]gwacl.InputEndpoint) 1113 for _, endpoint := range endpoints { 1114 _, have := mapping[endpoint.Port] 1115 c.Assert(have, gc.Equals, false) 1116 mapping[endpoint.Port] = endpoint 1117 } 1118 return mapping 1119 } 1120 1121 func (s *environSuite) TestNewRole(c *gc.C) { 1122 s.testNewRole(c, false) 1123 } 1124 1125 func (s *environSuite) TestNewRoleStateServer(c *gc.C) { 1126 s.testNewRole(c, true) 1127 } 1128 1129 func (*environSuite) testNewRole(c *gc.C, stateServer bool) { 1130 env := makeEnviron(c) 1131 size := "Large" 1132 vhd := env.newOSDisk("source-image-name") 1133 userData := "example-user-data" 1134 1135 role := env.newRole(size, vhd, userData, stateServer) 1136 1137 configs := role.ConfigurationSets 1138 linuxConfig := configs[0] 1139 networkConfig := configs[1] 1140 c.Check(linuxConfig.CustomData, gc.Equals, userData) 1141 c.Check(linuxConfig.Hostname, gc.Equals, role.RoleName) 1142 c.Check(linuxConfig.Username, gc.Not(gc.Equals), "") 1143 c.Check(linuxConfig.Password, gc.Not(gc.Equals), "") 1144 c.Check(linuxConfig.DisableSSHPasswordAuthentication, gc.Equals, "true") 1145 c.Check(role.RoleSize, gc.Equals, size) 1146 c.Check(role.OSVirtualHardDisk, gc.DeepEquals, vhd) 1147 1148 endpoints := mapInputEndpointsByPort(c, *networkConfig.InputEndpoints) 1149 1150 // The network config contains an endpoint for ssh communication. 1151 sshEndpoint, ok := endpoints[22] 1152 c.Assert(ok, gc.Equals, true) 1153 c.Check(sshEndpoint.LocalPort, gc.Equals, 22) 1154 c.Check(sshEndpoint.Protocol, gc.Equals, "tcp") 1155 1156 if stateServer { 1157 // There's also an endpoint for the state (mongodb) port. 1158 stateEndpoint, ok := endpoints[env.Config().StatePort()] 1159 c.Assert(ok, gc.Equals, true) 1160 c.Check(stateEndpoint.LocalPort, gc.Equals, env.Config().StatePort()) 1161 c.Check(stateEndpoint.Protocol, gc.Equals, "tcp") 1162 1163 // And one for the API port. 1164 apiEndpoint, ok := endpoints[env.Config().APIPort()] 1165 c.Assert(ok, gc.Equals, true) 1166 c.Check(apiEndpoint.LocalPort, gc.Equals, env.Config().APIPort()) 1167 c.Check(apiEndpoint.Protocol, gc.Equals, "tcp") 1168 } 1169 } 1170 1171 func (*environSuite) TestProviderReturnsAzureEnvironProvider(c *gc.C) { 1172 prov := makeEnviron(c).Provider() 1173 c.Assert(prov, gc.NotNil) 1174 azprov, ok := prov.(azureEnvironProvider) 1175 c.Assert(ok, gc.Equals, true) 1176 c.Check(azprov, gc.NotNil) 1177 } 1178 1179 func (*environSuite) TestCreateVirtualNetwork(c *gc.C) { 1180 env := makeEnviron(c) 1181 responses := []gwacl.DispatcherResponse{ 1182 // No existing configuration found. 1183 gwacl.NewDispatcherResponse(nil, http.StatusNotFound, nil), 1184 // Accept upload of new configuration. 1185 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 1186 } 1187 requests := gwacl.PatchManagementAPIResponses(responses) 1188 1189 env.createVirtualNetwork() 1190 1191 c.Assert(*requests, gc.HasLen, 2) 1192 request := (*requests)[1] 1193 body := gwacl.NetworkConfiguration{} 1194 err := xml.Unmarshal(request.Payload, &body) 1195 c.Assert(err, gc.IsNil) 1196 networkConf := (*body.VirtualNetworkSites)[0] 1197 c.Check(networkConf.Name, gc.Equals, env.getVirtualNetworkName()) 1198 c.Check(networkConf.AffinityGroup, gc.Equals, env.getAffinityGroupName()) 1199 } 1200 1201 func (*environSuite) TestDestroyVirtualNetwork(c *gc.C) { 1202 env := makeEnviron(c) 1203 // Prepare a configuration with a single virtual network. 1204 existingConfig := &gwacl.NetworkConfiguration{ 1205 XMLNS: gwacl.XMLNS_NC, 1206 VirtualNetworkSites: &[]gwacl.VirtualNetworkSite{ 1207 {Name: env.getVirtualNetworkName()}, 1208 }, 1209 } 1210 body, err := existingConfig.Serialize() 1211 c.Assert(err, gc.IsNil) 1212 responses := []gwacl.DispatcherResponse{ 1213 // Return existing configuration. 1214 gwacl.NewDispatcherResponse([]byte(body), http.StatusOK, nil), 1215 // Accept upload of new configuration. 1216 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 1217 } 1218 requests := gwacl.PatchManagementAPIResponses(responses) 1219 1220 env.deleteVirtualNetwork() 1221 1222 c.Assert(*requests, gc.HasLen, 2) 1223 // One request to get the existing network configuration. 1224 getRequest := (*requests)[0] 1225 c.Check(getRequest.Method, gc.Equals, "GET") 1226 // One request to update the network configuration. 1227 putRequest := (*requests)[1] 1228 c.Check(putRequest.Method, gc.Equals, "PUT") 1229 newConfig := gwacl.NetworkConfiguration{} 1230 err = xml.Unmarshal(putRequest.Payload, &newConfig) 1231 c.Assert(err, gc.IsNil) 1232 // The new configuration has no VirtualNetworkSites. 1233 c.Check(newConfig.VirtualNetworkSites, gc.IsNil) 1234 } 1235 1236 func (*environSuite) TestGetVirtualNetworkNameContainsEnvName(c *gc.C) { 1237 env := makeEnviron(c) 1238 c.Check(strings.Contains(env.getVirtualNetworkName(), env.Name()), jc.IsTrue) 1239 } 1240 1241 func (*environSuite) TestGetVirtualNetworkNameIsConstant(c *gc.C) { 1242 env := makeEnviron(c) 1243 c.Check(env.getVirtualNetworkName(), gc.Equals, env.getVirtualNetworkName()) 1244 } 1245 1246 func (*environSuite) TestCreateAffinityGroup(c *gc.C) { 1247 env := makeEnviron(c) 1248 responses := []gwacl.DispatcherResponse{ 1249 gwacl.NewDispatcherResponse(nil, http.StatusCreated, nil), 1250 } 1251 requests := gwacl.PatchManagementAPIResponses(responses) 1252 1253 env.createAffinityGroup() 1254 1255 c.Assert(*requests, gc.HasLen, 1) 1256 request := (*requests)[0] 1257 body := gwacl.CreateAffinityGroup{} 1258 err := xml.Unmarshal(request.Payload, &body) 1259 c.Assert(err, gc.IsNil) 1260 c.Check(body.Name, gc.Equals, env.getAffinityGroupName()) 1261 // This is a testing antipattern, the expected data comes from 1262 // config defaults. Fix it sometime. 1263 c.Check(body.Location, gc.Equals, "location") 1264 } 1265 1266 func (*environSuite) TestDestroyAffinityGroup(c *gc.C) { 1267 env := makeEnviron(c) 1268 responses := []gwacl.DispatcherResponse{ 1269 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 1270 } 1271 requests := gwacl.PatchManagementAPIResponses(responses) 1272 1273 env.deleteAffinityGroup() 1274 1275 c.Assert(*requests, gc.HasLen, 1) 1276 request := (*requests)[0] 1277 c.Check(strings.Contains(request.URL, env.getAffinityGroupName()), jc.IsTrue) 1278 c.Check(request.Method, gc.Equals, "DELETE") 1279 } 1280 1281 func (*environSuite) TestGetAffinityGroupName(c *gc.C) { 1282 env := makeEnviron(c) 1283 c.Check(strings.Contains(env.getAffinityGroupName(), env.Name()), jc.IsTrue) 1284 } 1285 1286 func (*environSuite) TestGetAffinityGroupNameIsConstant(c *gc.C) { 1287 env := makeEnviron(c) 1288 c.Check(env.getAffinityGroupName(), gc.Equals, env.getAffinityGroupName()) 1289 } 1290 1291 func (*environSuite) TestGetImageMetadataSigningRequiredDefaultsToTrue(c *gc.C) { 1292 env := makeEnviron(c) 1293 // Hard-coded to true for now. Once we support other base URLs, this 1294 // may have to become configurable. 1295 c.Check(env.getImageMetadataSigningRequired(), gc.Equals, true) 1296 } 1297 1298 func (*environSuite) TestSelectInstanceTypeAndImageUsesForcedImage(c *gc.C) { 1299 env := makeEnviron(c) 1300 forcedImage := "my-image" 1301 env.ecfg.attrs["force-image-name"] = forcedImage 1302 1303 aim := gwacl.RoleNameMap["ExtraLarge"] 1304 cons := constraints.Value{ 1305 CpuCores: &aim.CpuCores, 1306 Mem: &aim.Mem, 1307 } 1308 1309 instanceType, image, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ 1310 Region: "West US", 1311 Series: "precise", 1312 Constraints: cons, 1313 }) 1314 c.Assert(err, gc.IsNil) 1315 1316 c.Check(instanceType, gc.Equals, aim.Name) 1317 c.Check(image, gc.Equals, forcedImage) 1318 } 1319 1320 func (s *environSuite) setupEnvWithDummyMetadata(c *gc.C) *azureEnviron { 1321 envAttrs := makeAzureConfigMap(c) 1322 envAttrs["location"] = "North Europe" 1323 env := makeEnvironWithConfig(c, envAttrs) 1324 s.setDummyStorage(c, env) 1325 s.PatchValue(&imagemetadata.DefaultBaseURL, "") 1326 s.PatchValue(&signedImageDataOnly, false) 1327 images := []*imagemetadata.ImageMetadata{ 1328 { 1329 Id: "image-id", 1330 VirtType: "Hyper-V", 1331 Arch: "amd64", 1332 RegionName: "North Europe", 1333 Endpoint: "https://management.core.windows.net/", 1334 }, 1335 } 1336 makeTestMetadata(c, env, "precise", "North Europe", images) 1337 return env 1338 } 1339 1340 func (s *environSuite) TestSelectInstanceTypeAndImageUsesSimplestreamsByDefault(c *gc.C) { 1341 env := s.setupEnvWithDummyMetadata(c) 1342 // We'll tailor our constraints so as to get a specific instance type. 1343 aim := gwacl.RoleNameMap["ExtraSmall"] 1344 cons := constraints.Value{ 1345 CpuCores: &aim.CpuCores, 1346 Mem: &aim.Mem, 1347 } 1348 instanceType, image, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ 1349 Region: "North Europe", 1350 Series: "precise", 1351 Constraints: cons, 1352 }) 1353 c.Assert(err, gc.IsNil) 1354 c.Assert(instanceType, gc.Equals, aim.Name) 1355 c.Assert(image, gc.Equals, "image-id") 1356 } 1357 1358 func (*environSuite) TestExtractStorageKeyPicksPrimaryKeyIfSet(c *gc.C) { 1359 keys := gwacl.StorageAccountKeys{ 1360 Primary: "mainkey", 1361 Secondary: "otherkey", 1362 } 1363 c.Check(extractStorageKey(&keys), gc.Equals, "mainkey") 1364 } 1365 1366 func (*environSuite) TestExtractStorageKeyFallsBackToSecondaryKey(c *gc.C) { 1367 keys := gwacl.StorageAccountKeys{ 1368 Secondary: "sparekey", 1369 } 1370 c.Check(extractStorageKey(&keys), gc.Equals, "sparekey") 1371 } 1372 1373 func (*environSuite) TestExtractStorageKeyReturnsBlankIfNoneSet(c *gc.C) { 1374 c.Check(extractStorageKey(&gwacl.StorageAccountKeys{}), gc.Equals, "") 1375 } 1376 1377 func assertSourceContents(c *gc.C, source simplestreams.DataSource, filename string, content []byte) { 1378 rc, _, err := source.Fetch(filename) 1379 c.Assert(err, gc.IsNil) 1380 defer rc.Close() 1381 retrieved, err := ioutil.ReadAll(rc) 1382 c.Assert(err, gc.IsNil) 1383 c.Assert(retrieved, gc.DeepEquals, content) 1384 } 1385 1386 func (s *environSuite) assertGetImageMetadataSources(c *gc.C, stream, officialSourcePath string) { 1387 envAttrs := makeAzureConfigMap(c) 1388 if stream != "" { 1389 envAttrs["image-stream"] = stream 1390 } 1391 env := makeEnvironWithConfig(c, envAttrs) 1392 s.setDummyStorage(c, env) 1393 1394 data := []byte{1, 2, 3, 4} 1395 env.Storage().Put("images/filename", bytes.NewReader(data), int64(len(data))) 1396 1397 sources, err := imagemetadata.GetMetadataSources(env) 1398 c.Assert(err, gc.IsNil) 1399 c.Assert(len(sources), gc.Equals, 2) 1400 assertSourceContents(c, sources[0], "filename", data) 1401 url, err := sources[1].URL("") 1402 c.Assert(err, gc.IsNil) 1403 c.Assert(url, gc.Equals, fmt.Sprintf("http://cloud-images.ubuntu.com/%s/", officialSourcePath)) 1404 } 1405 1406 func (s *environSuite) TestGetImageMetadataSources(c *gc.C) { 1407 s.assertGetImageMetadataSources(c, "", "releases") 1408 s.assertGetImageMetadataSources(c, "released", "releases") 1409 s.assertGetImageMetadataSources(c, "daily", "daily") 1410 } 1411 1412 func (s *environSuite) TestGetToolsMetadataSources(c *gc.C) { 1413 env := makeEnviron(c) 1414 s.setDummyStorage(c, env) 1415 1416 data := []byte{1, 2, 3, 4} 1417 env.Storage().Put("tools/filename", bytes.NewReader(data), int64(len(data))) 1418 1419 sources, err := tools.GetMetadataSources(env) 1420 c.Assert(err, gc.IsNil) 1421 c.Assert(len(sources), gc.Equals, 1) 1422 assertSourceContents(c, sources[0], "filename", data) 1423 } 1424 1425 func (s *environSuite) TestCheckUnitAssignment(c *gc.C) { 1426 // If availability-sets-enabled is true, then placement is disabled. 1427 attrs := makeAzureConfigMap(c) 1428 attrs["availability-sets-enabled"] = true 1429 env := environs.Environ(makeEnvironWithConfig(c, attrs)) 1430 err := env.SupportsUnitPlacement() 1431 c.Assert(err, gc.ErrorMatches, "unit placement is not supported with availability-sets-enabled") 1432 1433 // If the user disables availability sets, they can do what they want. 1434 attrs["availability-sets-enabled"] = false 1435 env = environs.Environ(makeEnvironWithConfig(c, attrs)) 1436 err = env.SupportsUnitPlacement() 1437 c.Assert(err, gc.IsNil) 1438 } 1439 1440 type startInstanceSuite struct { 1441 baseEnvironSuite 1442 env *azureEnviron 1443 params environs.StartInstanceParams 1444 } 1445 1446 func (s *startInstanceSuite) SetUpTest(c *gc.C) { 1447 s.baseEnvironSuite.SetUpTest(c) 1448 s.env = makeEnviron(c) 1449 s.setDummyStorage(c, s.env) 1450 s.env.ecfg.attrs["force-image-name"] = "my-image" 1451 stateInfo := &state.Info{ 1452 Info: mongo.Info{ 1453 CACert: coretesting.CACert, 1454 Addrs: []string{"localhost:123"}, 1455 }, 1456 Password: "password", 1457 Tag: "machine-1", 1458 } 1459 apiInfo := &api.Info{ 1460 Addrs: []string{"localhost:124"}, 1461 CACert: coretesting.CACert, 1462 Password: "admin", 1463 Tag: "machine-1", 1464 } 1465 s.params = environs.StartInstanceParams{ 1466 Tools: envtesting.AssertUploadFakeToolsVersions( 1467 c, s.env.storage, envtesting.V120p..., 1468 ), 1469 MachineConfig: environs.NewMachineConfig( 1470 "1", "yanonce", nil, stateInfo, apiInfo, 1471 ), 1472 } 1473 } 1474 1475 func (s *startInstanceSuite) startInstance(c *gc.C) (serviceName string, stateServer bool) { 1476 var called bool 1477 restore := testing.PatchValue(&createInstance, func(env *azureEnviron, azure *gwacl.ManagementAPI, role *gwacl.Role, serviceNameArg string, stateServerArg bool) (instance.Instance, error) { 1478 serviceName = serviceNameArg 1479 stateServer = stateServerArg 1480 called = true 1481 return nil, nil 1482 }) 1483 defer restore() 1484 _, _, _, err := s.env.StartInstance(s.params) 1485 c.Assert(err, gc.IsNil) 1486 c.Assert(called, jc.IsTrue) 1487 return serviceName, stateServer 1488 } 1489 1490 func (s *startInstanceSuite) TestStartInstanceDistributionGroupError(c *gc.C) { 1491 s.params.DistributionGroup = func() ([]instance.Id, error) { 1492 return nil, fmt.Errorf("DistributionGroupError") 1493 } 1494 s.env.ecfg.attrs["availability-sets-enabled"] = true 1495 _, _, _, err := s.env.StartInstance(s.params) 1496 c.Assert(err, gc.ErrorMatches, "DistributionGroupError") 1497 // DistributionGroup should not be called if availability-sets-enabled=false. 1498 s.env.ecfg.attrs["availability-sets-enabled"] = false 1499 s.startInstance(c) 1500 } 1501 1502 func (s *startInstanceSuite) TestStartInstanceDistributionGroupEmpty(c *gc.C) { 1503 // serviceName will be empty if DistributionGroup is nil or returns nothing. 1504 s.env.ecfg.attrs["availability-sets-enabled"] = true 1505 serviceName, _ := s.startInstance(c) 1506 c.Assert(serviceName, gc.Equals, "") 1507 s.params.DistributionGroup = func() ([]instance.Id, error) { return nil, nil } 1508 serviceName, _ = s.startInstance(c) 1509 c.Assert(serviceName, gc.Equals, "") 1510 } 1511 1512 func (s *startInstanceSuite) TestStartInstanceDistributionGroup(c *gc.C) { 1513 s.params.DistributionGroup = func() ([]instance.Id, error) { 1514 return []instance.Id{ 1515 instance.Id(s.env.getEnvPrefix() + "whatever-role0"), 1516 }, nil 1517 } 1518 // DistributionGroup will only have an effect if 1519 // availability-sets-enabled=true. 1520 s.env.ecfg.attrs["availability-sets-enabled"] = false 1521 serviceName, _ := s.startInstance(c) 1522 c.Assert(serviceName, gc.Equals, "") 1523 s.env.ecfg.attrs["availability-sets-enabled"] = true 1524 serviceName, _ = s.startInstance(c) 1525 c.Assert(serviceName, gc.Equals, "juju-testenv-whatever") 1526 } 1527 1528 func (s *startInstanceSuite) TestStartInstanceStateServerJobs(c *gc.C) { 1529 // If the machine has the JobManagesEnviron job, 1530 // we should see stateServer==true. 1531 s.params.MachineConfig.Jobs = []apiparams.MachineJob{ 1532 apiparams.JobHostUnits, 1533 } 1534 _, stateServer := s.startInstance(c) 1535 c.Assert(stateServer, jc.IsFalse) 1536 s.params.MachineConfig.Jobs = []apiparams.MachineJob{ 1537 apiparams.JobHostUnits, apiparams.JobManageEnviron, 1538 } 1539 _, stateServer = s.startInstance(c) 1540 c.Assert(stateServer, jc.IsTrue) 1541 } 1542 1543 func (s *environSuite) TestConstraintsValidator(c *gc.C) { 1544 env := s.setupEnvWithDummyMetadata(c) 1545 validator, err := env.ConstraintsValidator() 1546 c.Assert(err, gc.IsNil) 1547 cons := constraints.MustParse("arch=amd64 tags=bar cpu-power=10") 1548 unsupported, err := validator.Validate(cons) 1549 c.Assert(err, gc.IsNil) 1550 c.Assert(unsupported, jc.SameContents, []string{"cpu-power", "tags"}) 1551 } 1552 1553 func (s *environSuite) TestConstraintsValidatorVocab(c *gc.C) { 1554 env := s.setupEnvWithDummyMetadata(c) 1555 validator, err := env.ConstraintsValidator() 1556 c.Assert(err, gc.IsNil) 1557 cons := constraints.MustParse("arch=ppc64") 1558 _, err = validator.Validate(cons) 1559 c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64\nvalid values are:.*") 1560 cons = constraints.MustParse("instance-type=foo") 1561 _, err = validator.Validate(cons) 1562 c.Assert(err, gc.ErrorMatches, "invalid constraint value: instance-type=foo\nvalid values are:.*") 1563 }