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