github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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 gc "launchpad.net/gocheck" 20 "launchpad.net/gwacl" 21 22 "launchpad.net/juju-core/constraints" 23 "launchpad.net/juju-core/environs" 24 "launchpad.net/juju-core/environs/bootstrap" 25 "launchpad.net/juju-core/environs/config" 26 "launchpad.net/juju-core/environs/imagemetadata" 27 "launchpad.net/juju-core/environs/simplestreams" 28 "launchpad.net/juju-core/environs/storage" 29 envtesting "launchpad.net/juju-core/environs/testing" 30 "launchpad.net/juju-core/environs/tools" 31 "launchpad.net/juju-core/instance" 32 "launchpad.net/juju-core/testing" 33 jc "launchpad.net/juju-core/testing/checkers" 34 ) 35 36 type environSuite struct { 37 providerSuite 38 } 39 40 var _ = gc.Suite(&environSuite{}) 41 42 // makeEnviron creates a fake azureEnviron with arbitrary configuration. 43 func makeEnviron(c *gc.C) *azureEnviron { 44 attrs := makeAzureConfigMap(c) 45 return makeEnvironWithConfig(c, attrs) 46 } 47 48 // makeEnviron creates a fake azureEnviron with the specified configuration. 49 func makeEnvironWithConfig(c *gc.C, attrs map[string]interface{}) *azureEnviron { 50 cfg, err := config.New(config.NoDefaults, attrs) 51 c.Assert(err, gc.IsNil) 52 env, err := NewEnviron(cfg) 53 c.Assert(err, gc.IsNil) 54 // Prevent the test from trying to query for a storage-account key. 55 env.storageAccountKey = "fake-storage-account-key" 56 return env 57 } 58 59 // setDummyStorage injects the local provider's fake storage implementation 60 // into the given environment, so that tests can manipulate storage as if it 61 // were real. 62 func (s *environSuite) setDummyStorage(c *gc.C, env *azureEnviron) { 63 closer, storage, _ := envtesting.CreateLocalTestStorage(c) 64 env.storage = storage 65 s.AddCleanup(func(c *gc.C) { closer.Close() }) 66 } 67 68 func (*environSuite) TestGetSnapshot(c *gc.C) { 69 original := azureEnviron{name: "this-env", ecfg: new(azureEnvironConfig)} 70 snapshot := original.getSnapshot() 71 72 // The snapshot is identical to the original. 73 c.Check(*snapshot, gc.DeepEquals, original) 74 75 // However, they are distinct objects. 76 c.Check(snapshot, gc.Not(gc.Equals), &original) 77 78 // It's a shallow copy; they still share pointers. 79 c.Check(snapshot.ecfg, gc.Equals, original.ecfg) 80 81 // Neither object is locked at the end of the copy. 82 c.Check(original.Mutex, gc.Equals, sync.Mutex{}) 83 c.Check(snapshot.Mutex, gc.Equals, sync.Mutex{}) 84 } 85 86 func (*environSuite) TestGetSnapshotLocksEnviron(c *gc.C) { 87 original := azureEnviron{} 88 testing.TestLockingFunction(&original.Mutex, func() { original.getSnapshot() }) 89 } 90 91 func (*environSuite) TestName(c *gc.C) { 92 env := azureEnviron{name: "foo"} 93 c.Check(env.Name(), gc.Equals, env.name) 94 } 95 96 func (*environSuite) TestConfigReturnsConfig(c *gc.C) { 97 cfg := new(config.Config) 98 ecfg := azureEnvironConfig{Config: cfg} 99 env := azureEnviron{ecfg: &ecfg} 100 c.Check(env.Config(), gc.Equals, cfg) 101 } 102 103 func (*environSuite) TestConfigLocksEnviron(c *gc.C) { 104 env := azureEnviron{name: "env", ecfg: new(azureEnvironConfig)} 105 testing.TestLockingFunction(&env.Mutex, func() { env.Config() }) 106 } 107 108 func (*environSuite) TestGetManagementAPI(c *gc.C) { 109 env := makeEnviron(c) 110 context, err := env.getManagementAPI() 111 c.Assert(err, gc.IsNil) 112 defer env.releaseManagementAPI(context) 113 c.Check(context, gc.NotNil) 114 c.Check(context.ManagementAPI, gc.NotNil) 115 c.Check(context.certFile, gc.NotNil) 116 c.Check(context.GetRetryPolicy(), gc.DeepEquals, retryPolicy) 117 } 118 119 func (*environSuite) TestReleaseManagementAPIAcceptsNil(c *gc.C) { 120 env := makeEnviron(c) 121 env.releaseManagementAPI(nil) 122 // The real test is that this does not panic. 123 } 124 125 func (*environSuite) TestReleaseManagementAPIAcceptsIncompleteContext(c *gc.C) { 126 env := makeEnviron(c) 127 context := azureManagementContext{ 128 ManagementAPI: nil, 129 certFile: nil, 130 } 131 env.releaseManagementAPI(&context) 132 // The real test is that this does not panic. 133 } 134 135 func getAzureServiceListResponse(c *gc.C, services []gwacl.HostedServiceDescriptor) []gwacl.DispatcherResponse { 136 list := gwacl.HostedServiceDescriptorList{HostedServices: services} 137 listXML, err := list.Serialize() 138 c.Assert(err, gc.IsNil) 139 responses := []gwacl.DispatcherResponse{gwacl.NewDispatcherResponse( 140 []byte(listXML), 141 http.StatusOK, 142 nil, 143 )} 144 return responses 145 } 146 147 // getAzureServiceResponses returns the slice of responses 148 // (gwacl.DispatcherResponse) which correspond to the API requests used to 149 // get the properties of a Service. 150 func getAzureServiceResponses(c *gc.C, service gwacl.HostedService) []gwacl.DispatcherResponse { 151 serviceXML, err := service.Serialize() 152 c.Assert(err, gc.IsNil) 153 responses := []gwacl.DispatcherResponse{gwacl.NewDispatcherResponse( 154 []byte(serviceXML), 155 http.StatusOK, 156 nil, 157 )} 158 return responses 159 } 160 161 func patchWithServiceListResponse(c *gc.C, services []gwacl.HostedServiceDescriptor) *[]*gwacl.X509Request { 162 responses := getAzureServiceListResponse(c, services) 163 return gwacl.PatchManagementAPIResponses(responses) 164 } 165 166 func (suite *environSuite) TestGetEnvPrefixContainsEnvName(c *gc.C) { 167 env := makeEnviron(c) 168 c.Check(strings.Contains(env.getEnvPrefix(), env.Name()), jc.IsTrue) 169 } 170 171 func (*environSuite) TestGetContainerName(c *gc.C) { 172 env := makeEnviron(c) 173 expected := env.getEnvPrefix() + "private" 174 c.Check(env.getContainerName(), gc.Equals, expected) 175 } 176 177 func (suite *environSuite) TestAllInstances(c *gc.C) { 178 env := makeEnviron(c) 179 prefix := env.getEnvPrefix() 180 services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-in-another-env"}, {ServiceName: prefix + "deployment-1"}, {ServiceName: prefix + "deployment-2"}} 181 requests := patchWithServiceListResponse(c, services) 182 instances, err := env.AllInstances() 183 c.Assert(err, gc.IsNil) 184 c.Check(len(instances), gc.Equals, 2) 185 c.Check(instances[0].Id(), gc.Equals, instance.Id(prefix+"deployment-1")) 186 c.Check(instances[1].Id(), gc.Equals, instance.Id(prefix+"deployment-2")) 187 c.Check(len(*requests), gc.Equals, 1) 188 } 189 190 func (suite *environSuite) TestInstancesReturnsFilteredList(c *gc.C) { 191 services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-1"}, {ServiceName: "deployment-2"}} 192 requests := patchWithServiceListResponse(c, services) 193 env := makeEnviron(c) 194 instances, err := env.Instances([]instance.Id{"deployment-1"}) 195 c.Assert(err, gc.IsNil) 196 c.Check(len(instances), gc.Equals, 1) 197 c.Check(instances[0].Id(), gc.Equals, instance.Id("deployment-1")) 198 c.Check(len(*requests), gc.Equals, 1) 199 } 200 201 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNoInstancesRequested(c *gc.C) { 202 services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-1"}, {ServiceName: "deployment-2"}} 203 patchWithServiceListResponse(c, services) 204 env := makeEnviron(c) 205 instances, err := env.Instances([]instance.Id{}) 206 c.Check(err, gc.Equals, environs.ErrNoInstances) 207 c.Check(instances, gc.IsNil) 208 } 209 210 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNoInstanceFound(c *gc.C) { 211 services := []gwacl.HostedServiceDescriptor{} 212 patchWithServiceListResponse(c, services) 213 env := makeEnviron(c) 214 instances, err := env.Instances([]instance.Id{"deploy-id"}) 215 c.Check(err, gc.Equals, environs.ErrNoInstances) 216 c.Check(instances, gc.IsNil) 217 } 218 219 func (suite *environSuite) TestInstancesReturnsPartialInstancesIfSomeInstancesAreNotFound(c *gc.C) { 220 services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-1"}, {ServiceName: "deployment-2"}} 221 requests := patchWithServiceListResponse(c, services) 222 env := makeEnviron(c) 223 instances, err := env.Instances([]instance.Id{"deployment-1", "unknown-deployment"}) 224 c.Assert(err, gc.Equals, environs.ErrPartialInstances) 225 c.Check(len(instances), gc.Equals, 1) 226 c.Check(instances[0].Id(), gc.Equals, instance.Id("deployment-1")) 227 c.Check(len(*requests), gc.Equals, 1) 228 } 229 230 func (*environSuite) TestStorage(c *gc.C) { 231 env := makeEnviron(c) 232 baseStorage := env.Storage() 233 storage, ok := baseStorage.(*azureStorage) 234 c.Check(ok, gc.Equals, true) 235 c.Assert(storage, gc.NotNil) 236 c.Check(storage.storageContext.getContainer(), gc.Equals, env.getContainerName()) 237 context, err := storage.getStorageContext() 238 c.Assert(err, gc.IsNil) 239 c.Check(context.Account, gc.Equals, env.ecfg.storageAccountName()) 240 c.Check(context.RetryPolicy, gc.DeepEquals, retryPolicy) 241 } 242 243 func (*environSuite) TestQueryStorageAccountKeyGetsKey(c *gc.C) { 244 env := makeEnviron(c) 245 keysInAzure := gwacl.StorageAccountKeys{Primary: "a-key"} 246 azureResponse, err := xml.Marshal(keysInAzure) 247 c.Assert(err, gc.IsNil) 248 requests := gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 249 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 250 }) 251 252 returnedKey, err := env.queryStorageAccountKey() 253 c.Assert(err, gc.IsNil) 254 255 c.Check(returnedKey, gc.Equals, keysInAzure.Primary) 256 c.Assert(*requests, gc.HasLen, 1) 257 c.Check((*requests)[0].Method, gc.Equals, "GET") 258 } 259 260 func (*environSuite) TestGetStorageContextCreatesStorageContext(c *gc.C) { 261 env := makeEnviron(c) 262 stor, err := env.getStorageContext() 263 c.Assert(err, gc.IsNil) 264 c.Assert(stor, gc.NotNil) 265 c.Check(stor.Account, gc.Equals, env.ecfg.storageAccountName()) 266 c.Check(stor.AzureEndpoint, gc.Equals, gwacl.GetEndpoint(env.ecfg.location())) 267 } 268 269 func (*environSuite) TestGetStorageContextUsesKnownStorageAccountKey(c *gc.C) { 270 env := makeEnviron(c) 271 env.storageAccountKey = "my-key" 272 273 stor, err := env.getStorageContext() 274 c.Assert(err, gc.IsNil) 275 276 c.Check(stor.Key, gc.Equals, "my-key") 277 } 278 279 func (*environSuite) TestGetStorageContextQueriesStorageAccountKeyIfNeeded(c *gc.C) { 280 env := makeEnviron(c) 281 env.storageAccountKey = "" 282 keysInAzure := gwacl.StorageAccountKeys{Primary: "my-key"} 283 azureResponse, err := xml.Marshal(keysInAzure) 284 c.Assert(err, gc.IsNil) 285 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 286 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 287 }) 288 289 stor, err := env.getStorageContext() 290 c.Assert(err, gc.IsNil) 291 292 c.Check(stor.Key, gc.Equals, keysInAzure.Primary) 293 c.Check(env.storageAccountKey, gc.Equals, keysInAzure.Primary) 294 } 295 296 func (*environSuite) TestGetStorageContextFailsIfNoKeyAvailable(c *gc.C) { 297 env := makeEnviron(c) 298 env.storageAccountKey = "" 299 azureResponse, err := xml.Marshal(gwacl.StorageAccountKeys{}) 300 c.Assert(err, gc.IsNil) 301 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 302 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 303 }) 304 305 _, err = env.getStorageContext() 306 c.Assert(err, gc.NotNil) 307 308 c.Check(err, gc.ErrorMatches, "no keys available for storage account") 309 } 310 311 func (*environSuite) TestUpdateStorageAccountKeyGetsFreshKey(c *gc.C) { 312 env := makeEnviron(c) 313 keysInAzure := gwacl.StorageAccountKeys{Primary: "my-key"} 314 azureResponse, err := xml.Marshal(keysInAzure) 315 c.Assert(err, gc.IsNil) 316 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 317 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 318 }) 319 320 key, err := env.updateStorageAccountKey(env.getSnapshot()) 321 c.Assert(err, gc.IsNil) 322 323 c.Check(key, gc.Equals, keysInAzure.Primary) 324 c.Check(env.storageAccountKey, gc.Equals, keysInAzure.Primary) 325 } 326 327 func (*environSuite) TestUpdateStorageAccountKeyReturnsError(c *gc.C) { 328 env := makeEnviron(c) 329 env.storageAccountKey = "" 330 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 331 gwacl.NewDispatcherResponse(nil, http.StatusInternalServerError, nil), 332 }) 333 334 _, err := env.updateStorageAccountKey(env.getSnapshot()) 335 c.Assert(err, gc.NotNil) 336 337 c.Check(err, gc.ErrorMatches, "cannot obtain storage account keys: GET request failed.*Internal Server Error.*") 338 c.Check(env.storageAccountKey, gc.Equals, "") 339 } 340 341 func (*environSuite) TestUpdateStorageAccountKeyDetectsConcurrentUpdate(c *gc.C) { 342 env := makeEnviron(c) 343 env.storageAccountKey = "" 344 keysInAzure := gwacl.StorageAccountKeys{Primary: "my-key"} 345 azureResponse, err := xml.Marshal(keysInAzure) 346 c.Assert(err, gc.IsNil) 347 gwacl.PatchManagementAPIResponses([]gwacl.DispatcherResponse{ 348 gwacl.NewDispatcherResponse(azureResponse, http.StatusOK, nil), 349 }) 350 351 // Here we use a snapshot that's different from the environment, to 352 // simulate a concurrent change to the environment. 353 _, err = env.updateStorageAccountKey(makeEnviron(c)) 354 c.Assert(err, gc.NotNil) 355 356 // updateStorageAccountKey detects the change, and refuses to write its 357 // outdated information into env. 358 c.Check(err, gc.ErrorMatches, "environment was reconfigured") 359 c.Check(env.storageAccountKey, gc.Equals, "") 360 } 361 362 func (*environSuite) TestSetConfigValidates(c *gc.C) { 363 env := makeEnviron(c) 364 originalCfg := env.ecfg 365 attrs := makeAzureConfigMap(c) 366 // This config is not valid. It lacks essential information. 367 delete(attrs, "management-subscription-id") 368 badCfg, err := config.New(config.NoDefaults, attrs) 369 c.Assert(err, gc.IsNil) 370 371 err = env.SetConfig(badCfg) 372 373 // Since the config was not valid, SetConfig returns an error. It 374 // does not update the environment's config either. 375 c.Check(err, gc.NotNil) 376 c.Check( 377 err, 378 gc.ErrorMatches, 379 "management-subscription-id: expected string, got nothing") 380 c.Check(env.ecfg, gc.Equals, originalCfg) 381 } 382 383 func (*environSuite) TestSetConfigUpdatesConfig(c *gc.C) { 384 env := makeEnviron(c) 385 // We're going to set a new config. It can be recognized by its 386 // unusual default Ubuntu release series: 7.04 Feisty Fawn. 387 attrs := makeAzureConfigMap(c) 388 attrs["default-series"] = "feisty" 389 cfg, err := config.New(config.NoDefaults, attrs) 390 c.Assert(err, gc.IsNil) 391 392 err = env.SetConfig(cfg) 393 c.Assert(err, gc.IsNil) 394 395 c.Check(env.ecfg.Config.DefaultSeries(), gc.Equals, "feisty") 396 } 397 398 func (*environSuite) TestSetConfigLocksEnviron(c *gc.C) { 399 env := makeEnviron(c) 400 cfg, err := config.New(config.NoDefaults, makeAzureConfigMap(c)) 401 c.Assert(err, gc.IsNil) 402 403 testing.TestLockingFunction(&env.Mutex, func() { env.SetConfig(cfg) }) 404 } 405 406 func (*environSuite) TestSetConfigWillNotUpdateName(c *gc.C) { 407 // Once the environment's name has been set, it cannot be updated. 408 // Global validation rejects such a change. 409 // This matters because the attribute is not protected by a lock. 410 env := makeEnviron(c) 411 originalName := env.Name() 412 attrs := makeAzureConfigMap(c) 413 attrs["name"] = "new-name" 414 cfg, err := config.New(config.NoDefaults, attrs) 415 c.Assert(err, gc.IsNil) 416 417 err = env.SetConfig(cfg) 418 419 c.Assert(err, gc.NotNil) 420 c.Check( 421 err, 422 gc.ErrorMatches, 423 `cannot change name from ".*" to "new-name"`) 424 c.Check(env.Name(), gc.Equals, originalName) 425 } 426 427 func (*environSuite) TestSetConfigClearsStorageAccountKey(c *gc.C) { 428 env := makeEnviron(c) 429 env.storageAccountKey = "key-for-previous-config" 430 attrs := makeAzureConfigMap(c) 431 attrs["default-series"] = "other" 432 cfg, err := config.New(config.NoDefaults, attrs) 433 c.Assert(err, gc.IsNil) 434 435 err = env.SetConfig(cfg) 436 c.Assert(err, gc.IsNil) 437 438 c.Check(env.storageAccountKey, gc.Equals, "") 439 } 440 441 func (s *environSuite) TestStateInfoFailsIfNoStateInstances(c *gc.C) { 442 env := makeEnviron(c) 443 s.setDummyStorage(c, env) 444 _, _, err := env.StateInfo() 445 c.Check(err, gc.Equals, environs.ErrNotBootstrapped) 446 } 447 448 func (s *environSuite) TestStateInfo(c *gc.C) { 449 instanceID := "my-instance" 450 patchWithServiceListResponse(c, []gwacl.HostedServiceDescriptor{{ 451 ServiceName: instanceID, 452 }}) 453 env := makeEnviron(c) 454 s.setDummyStorage(c, env) 455 err := bootstrap.SaveState( 456 env.Storage(), 457 &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id(instanceID)}}) 458 c.Assert(err, gc.IsNil) 459 460 stateInfo, apiInfo, err := env.StateInfo() 461 c.Assert(err, gc.IsNil) 462 463 config := env.Config() 464 dnsName := "my-instance." + AZURE_DOMAIN_NAME 465 stateServerAddr := fmt.Sprintf("%s:%d", dnsName, config.StatePort()) 466 apiServerAddr := fmt.Sprintf("%s:%d", dnsName, config.APIPort()) 467 c.Check(stateInfo.Addrs, gc.DeepEquals, []string{stateServerAddr}) 468 c.Check(apiInfo.Addrs, gc.DeepEquals, []string{apiServerAddr}) 469 } 470 471 // parseCreateServiceRequest reconstructs the original CreateHostedService 472 // request object passed to gwacl's AddHostedService method, based on the 473 // X509Request which the method issues. 474 func parseCreateServiceRequest(c *gc.C, request *gwacl.X509Request) *gwacl.CreateHostedService { 475 body := gwacl.CreateHostedService{} 476 err := xml.Unmarshal(request.Payload, &body) 477 c.Assert(err, gc.IsNil) 478 return &body 479 } 480 481 // makeNonAvailabilityResponse simulates a reply to the 482 // CheckHostedServiceNameAvailability call saying that a name is not available. 483 func makeNonAvailabilityResponse(c *gc.C) []byte { 484 errorBody, err := xml.Marshal(gwacl.AvailabilityResponse{ 485 Result: "false", 486 Reason: "he's a very naughty boy"}) 487 c.Assert(err, gc.IsNil) 488 return errorBody 489 } 490 491 // makeAvailabilityResponse simulates a reply to the 492 // CheckHostedServiceNameAvailability call saying that a name is available. 493 func makeAvailabilityResponse(c *gc.C) []byte { 494 errorBody, err := xml.Marshal(gwacl.AvailabilityResponse{ 495 Result: "true"}) 496 c.Assert(err, gc.IsNil) 497 return errorBody 498 } 499 500 func (*environSuite) TestAttemptCreateServiceCreatesService(c *gc.C) { 501 prefix := "myservice" 502 affinityGroup := "affinity-group" 503 location := "location" 504 responses := []gwacl.DispatcherResponse{ 505 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil), 506 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 507 } 508 requests := gwacl.PatchManagementAPIResponses(responses) 509 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 510 c.Assert(err, gc.IsNil) 511 512 service, err := attemptCreateService(azure, prefix, affinityGroup, location) 513 c.Assert(err, gc.IsNil) 514 515 c.Assert(*requests, gc.HasLen, 2) 516 body := parseCreateServiceRequest(c, (*requests)[1]) 517 c.Check(body.ServiceName, gc.Equals, service.ServiceName) 518 c.Check(body.AffinityGroup, gc.Equals, affinityGroup) 519 c.Check(service.ServiceName, gc.Matches, prefix+".*") 520 c.Check(service.Location, gc.Equals, location) 521 522 label, err := base64.StdEncoding.DecodeString(service.Label) 523 c.Assert(err, gc.IsNil) 524 c.Check(string(label), gc.Equals, service.ServiceName) 525 } 526 527 func (*environSuite) TestAttemptCreateServiceReturnsNilIfNameNotUnique(c *gc.C) { 528 responses := []gwacl.DispatcherResponse{ 529 gwacl.NewDispatcherResponse(makeNonAvailabilityResponse(c), http.StatusOK, nil), 530 } 531 gwacl.PatchManagementAPIResponses(responses) 532 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 533 c.Assert(err, gc.IsNil) 534 535 service, err := attemptCreateService(azure, "service", "affinity-group", "location") 536 c.Check(err, gc.IsNil) 537 c.Check(service, gc.IsNil) 538 } 539 540 func (*environSuite) TestAttemptCreateServicePropagatesOtherFailure(c *gc.C) { 541 responses := []gwacl.DispatcherResponse{ 542 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil), 543 gwacl.NewDispatcherResponse(nil, http.StatusNotFound, nil), 544 } 545 gwacl.PatchManagementAPIResponses(responses) 546 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 547 c.Assert(err, gc.IsNil) 548 549 _, err = attemptCreateService(azure, "service", "affinity-group", "location") 550 c.Assert(err, gc.NotNil) 551 c.Check(err, gc.ErrorMatches, ".*Not Found.*") 552 } 553 554 func (*environSuite) TestNewHostedServiceCreatesService(c *gc.C) { 555 prefix := "myservice" 556 affinityGroup := "affinity-group" 557 location := "location" 558 responses := []gwacl.DispatcherResponse{ 559 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil), 560 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 561 } 562 requests := gwacl.PatchManagementAPIResponses(responses) 563 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 564 c.Assert(err, gc.IsNil) 565 566 service, err := newHostedService(azure, prefix, affinityGroup, location) 567 c.Assert(err, gc.IsNil) 568 569 c.Assert(*requests, gc.HasLen, 2) 570 body := parseCreateServiceRequest(c, (*requests)[1]) 571 c.Check(body.ServiceName, gc.Equals, service.ServiceName) 572 c.Check(body.AffinityGroup, gc.Equals, affinityGroup) 573 c.Check(service.ServiceName, gc.Matches, prefix+".*") 574 c.Check(service.Location, gc.Equals, location) 575 } 576 577 func (*environSuite) TestNewHostedServiceRetriesIfNotUnique(c *gc.C) { 578 errorBody := makeNonAvailabilityResponse(c) 579 okBody := makeAvailabilityResponse(c) 580 // In this scenario, the first two names that we try are already 581 // taken. The third one is unique though, so we succeed. 582 responses := []gwacl.DispatcherResponse{ 583 gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil), 584 gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil), 585 gwacl.NewDispatcherResponse(okBody, http.StatusOK, nil), 586 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 587 } 588 requests := gwacl.PatchManagementAPIResponses(responses) 589 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 590 c.Assert(err, gc.IsNil) 591 592 service, err := newHostedService(azure, "service", "affinity-group", "location") 593 c.Check(err, gc.IsNil) 594 595 c.Assert(*requests, gc.HasLen, 4) 596 // How many names have been attempted, and how often? 597 // There is a minute chance that this tries the same name twice, and 598 // then this test will fail. If that happens, try seeding the 599 // randomizer with some fixed seed that doens't produce the problem. 600 attemptedNames := make(map[string]int) 601 for _, request := range *requests { 602 // Exit the loop if we hit the request to create the service, it comes 603 // after the check calls. 604 if request.Method == "POST" { 605 break 606 } 607 // Name is the last part of the URL from the GET requests that check 608 // availability. 609 _, name := path.Split(strings.TrimRight(request.URL, "/")) 610 attemptedNames[name] += 1 611 } 612 // The three attempts we just made all had different service names. 613 c.Check(attemptedNames, gc.HasLen, 3) 614 615 // Once newHostedService succeeds, we get a hosted service with the 616 // last requested name. 617 c.Check( 618 service.ServiceName, 619 gc.Equals, 620 parseCreateServiceRequest(c, (*requests)[3]).ServiceName) 621 } 622 623 func (*environSuite) TestNewHostedServiceFailsIfUnableToFindUniqueName(c *gc.C) { 624 errorBody := makeNonAvailabilityResponse(c) 625 responses := []gwacl.DispatcherResponse{} 626 for counter := 0; counter < 100; counter++ { 627 responses = append(responses, gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil)) 628 } 629 gwacl.PatchManagementAPIResponses(responses) 630 azure, err := gwacl.NewManagementAPI("subscription", "", "West US") 631 c.Assert(err, gc.IsNil) 632 633 _, err = newHostedService(azure, "service", "affinity-group", "location") 634 c.Assert(err, gc.NotNil) 635 c.Check(err, gc.ErrorMatches, "could not come up with a unique hosted service name.*") 636 } 637 638 // buildDestroyAzureServiceResponses returns a slice containing the responses that a fake Azure server 639 // can use to simulate the deletion of the given list of services. 640 func buildDestroyAzureServiceResponses(c *gc.C, services []*gwacl.HostedService) []gwacl.DispatcherResponse { 641 responses := []gwacl.DispatcherResponse{} 642 for _, service := range services { 643 // When destroying a hosted service, gwacl first issues a Get request 644 // to fetch the properties of the services. Then it destroys all the 645 // deployments found in this service (none in this case, we make sure 646 // the service does not contain deployments to keep the testing simple) 647 // And it finally deletes the service itself. 648 if len(service.Deployments) != 0 { 649 panic("buildDestroyAzureServiceResponses does not support services with deployments!") 650 } 651 serviceXML, err := service.Serialize() 652 c.Assert(err, gc.IsNil) 653 serviceGetResponse := gwacl.NewDispatcherResponse( 654 []byte(serviceXML), 655 http.StatusOK, 656 nil, 657 ) 658 responses = append(responses, serviceGetResponse) 659 serviceDeleteResponse := gwacl.NewDispatcherResponse( 660 nil, 661 http.StatusOK, 662 nil, 663 ) 664 responses = append(responses, serviceDeleteResponse) 665 } 666 return responses 667 } 668 669 func makeAzureService(name string) (*gwacl.HostedService, *gwacl.HostedServiceDescriptor) { 670 service1Desc := &gwacl.HostedServiceDescriptor{ServiceName: name} 671 service1 := &gwacl.HostedService{HostedServiceDescriptor: *service1Desc} 672 return service1, service1Desc 673 } 674 675 func (s *environSuite) setServiceDeletionConcurrency(nbGoroutines int) { 676 s.PatchValue(&maxConcurrentDeletes, nbGoroutines) 677 } 678 679 func (s *environSuite) TestStopInstancesDestroysMachines(c *gc.C) { 680 s.setServiceDeletionConcurrency(3) 681 service1Name := "service1" 682 service1, service1Desc := makeAzureService(service1Name) 683 service2Name := "service2" 684 service2, service2Desc := makeAzureService(service2Name) 685 services := []*gwacl.HostedService{service1, service2} 686 responses := buildDestroyAzureServiceResponses(c, services) 687 requests := gwacl.PatchManagementAPIResponses(responses) 688 env := makeEnviron(c) 689 instances := convertToInstances( 690 []gwacl.HostedServiceDescriptor{*service1Desc, *service2Desc}, 691 env) 692 693 err := env.StopInstances(instances) 694 c.Check(err, gc.IsNil) 695 696 // It takes 2 API calls to delete each service: 697 // - one GET request to fetch the service's properties; 698 // - one DELETE request to delete the service. 699 c.Check(len(*requests), gc.Equals, len(services)*2) 700 assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".*") 701 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1Name+".*") 702 assertOneRequestMatches(c, *requests, "GET", ".*"+service2Name+".") 703 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service2Name+".*") 704 } 705 706 func (s *environSuite) TestStopInstancesWhenStoppingMachinesFails(c *gc.C) { 707 s.setServiceDeletionConcurrency(3) 708 responses := []gwacl.DispatcherResponse{ 709 gwacl.NewDispatcherResponse(nil, http.StatusConflict, nil), 710 } 711 service1Name := "service1" 712 _, service1Desc := makeAzureService(service1Name) 713 service2Name := "service2" 714 service2, service2Desc := makeAzureService(service2Name) 715 services := []*gwacl.HostedService{service2} 716 destroyResponses := buildDestroyAzureServiceResponses(c, services) 717 responses = append(responses, destroyResponses...) 718 requests := gwacl.PatchManagementAPIResponses(responses) 719 env := makeEnviron(c) 720 instances := convertToInstances( 721 []gwacl.HostedServiceDescriptor{*service1Desc, *service2Desc}, env) 722 723 err := env.StopInstances(instances) 724 c.Check(err, gc.ErrorMatches, ".*Conflict.*") 725 726 c.Check(len(*requests), gc.Equals, 3) 727 assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".") 728 assertOneRequestMatches(c, *requests, "GET", ".*"+service2Name+".") 729 // Only one of the services was deleted. 730 assertOneRequestMatches(c, *requests, "DELETE", ".*") 731 } 732 733 func (s *environSuite) TestStopInstancesWithLimitedConcurrency(c *gc.C) { 734 s.setServiceDeletionConcurrency(3) 735 services := []*gwacl.HostedService{} 736 serviceDescs := []gwacl.HostedServiceDescriptor{} 737 for i := 0; i < 10; i++ { 738 serviceName := fmt.Sprintf("service%d", i) 739 service, serviceDesc := makeAzureService(serviceName) 740 services = append(services, service) 741 serviceDescs = append(serviceDescs, *serviceDesc) 742 } 743 responses := buildDestroyAzureServiceResponses(c, services) 744 requests := gwacl.PatchManagementAPIResponses(responses) 745 env := makeEnviron(c) 746 instances := convertToInstances(serviceDescs, env) 747 748 err := env.StopInstances(instances) 749 c.Check(err, gc.IsNil) 750 c.Check(len(*requests), gc.Equals, len(services)*2) 751 } 752 753 func (s *environSuite) TestStopInstancesWithZeroInstance(c *gc.C) { 754 s.setServiceDeletionConcurrency(3) 755 env := makeEnviron(c) 756 instances := []instance.Instance{} 757 758 err := env.StopInstances(instances) 759 c.Check(err, gc.IsNil) 760 } 761 762 // getVnetAndAffinityGroupCleanupResponses returns the responses 763 // (gwacl.DispatcherResponse) that a fake http server should return 764 // when gwacl's RemoveVirtualNetworkSite() and DeleteAffinityGroup() 765 // are called. 766 func getVnetAndAffinityGroupCleanupResponses(c *gc.C) []gwacl.DispatcherResponse { 767 existingConfig := &gwacl.NetworkConfiguration{ 768 XMLNS: gwacl.XMLNS_NC, 769 VirtualNetworkSites: nil, 770 } 771 body, err := existingConfig.Serialize() 772 c.Assert(err, gc.IsNil) 773 cleanupResponses := []gwacl.DispatcherResponse{ 774 // Return empty net configuration. 775 gwacl.NewDispatcherResponse([]byte(body), http.StatusOK, nil), 776 // Accept deletion of affinity group. 777 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 778 } 779 return cleanupResponses 780 } 781 782 func (s *environSuite) TestDestroyDoesNotCleanStorageIfError(c *gc.C) { 783 env := makeEnviron(c) 784 s.setDummyStorage(c, env) 785 // Populate storage. 786 err := bootstrap.SaveState( 787 env.Storage(), 788 &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id("test-id")}}) 789 c.Assert(err, gc.IsNil) 790 responses := []gwacl.DispatcherResponse{ 791 gwacl.NewDispatcherResponse(nil, http.StatusBadRequest, nil), 792 } 793 gwacl.PatchManagementAPIResponses(responses) 794 795 err = env.Destroy() 796 c.Check(err, gc.NotNil) 797 798 files, err := storage.List(env.Storage(), "") 799 c.Assert(err, gc.IsNil) 800 c.Check(files, gc.HasLen, 1) 801 } 802 803 func (s *environSuite) TestDestroyCleansUpStorage(c *gc.C) { 804 env := makeEnviron(c) 805 s.setDummyStorage(c, env) 806 // Populate storage. 807 err := bootstrap.SaveState( 808 env.Storage(), 809 &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id("test-id")}}) 810 c.Assert(err, gc.IsNil) 811 services := []gwacl.HostedServiceDescriptor{} 812 responses := getAzureServiceListResponse(c, services) 813 cleanupResponses := getVnetAndAffinityGroupCleanupResponses(c) 814 responses = append(responses, cleanupResponses...) 815 gwacl.PatchManagementAPIResponses(responses) 816 817 err = env.Destroy() 818 c.Check(err, gc.IsNil) 819 820 files, err := storage.List(env.Storage(), "") 821 c.Assert(err, gc.IsNil) 822 c.Check(files, gc.HasLen, 0) 823 } 824 825 func (s *environSuite) TestDestroyDeletesVirtualNetworkAndAffinityGroup(c *gc.C) { 826 env := makeEnviron(c) 827 s.setDummyStorage(c, env) 828 services := []gwacl.HostedServiceDescriptor{} 829 responses := getAzureServiceListResponse(c, services) 830 // Prepare a configuration with a single virtual network. 831 existingConfig := &gwacl.NetworkConfiguration{ 832 XMLNS: gwacl.XMLNS_NC, 833 VirtualNetworkSites: &[]gwacl.VirtualNetworkSite{ 834 {Name: env.getVirtualNetworkName()}, 835 }, 836 } 837 body, err := existingConfig.Serialize() 838 c.Assert(err, gc.IsNil) 839 cleanupResponses := []gwacl.DispatcherResponse{ 840 // Return existing configuration. 841 gwacl.NewDispatcherResponse([]byte(body), http.StatusOK, nil), 842 // Accept upload of new configuration. 843 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 844 // Accept deletion of affinity group. 845 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 846 } 847 responses = append(responses, cleanupResponses...) 848 requests := gwacl.PatchManagementAPIResponses(responses) 849 850 err = env.Destroy() 851 c.Check(err, gc.IsNil) 852 853 c.Assert(*requests, gc.HasLen, 4) 854 // One request to get the network configuration. 855 getRequest := (*requests)[1] 856 c.Check(getRequest.Method, gc.Equals, "GET") 857 c.Check(strings.HasSuffix(getRequest.URL, "services/networking/media"), gc.Equals, true) 858 // One request to upload the new version of the network configuration. 859 putRequest := (*requests)[2] 860 c.Check(putRequest.Method, gc.Equals, "PUT") 861 c.Check(strings.HasSuffix(putRequest.URL, "services/networking/media"), gc.Equals, true) 862 // One request to delete the Affinity Group. 863 agRequest := (*requests)[3] 864 c.Check(strings.Contains(agRequest.URL, env.getAffinityGroupName()), jc.IsTrue) 865 c.Check(agRequest.Method, gc.Equals, "DELETE") 866 867 } 868 869 var emptyListResponse = ` 870 <?xml version="1.0" encoding="utf-8"?> 871 <EnumerationResults ContainerName="http://myaccount.blob.core.windows.net/mycontainer"> 872 <Prefix>prefix</Prefix> 873 <Marker>marker</Marker> 874 <MaxResults>maxresults</MaxResults> 875 <Delimiter>delimiter</Delimiter> 876 <Blobs></Blobs> 877 <NextMarker /> 878 </EnumerationResults>` 879 880 // assertOneRequestMatches asserts that at least one request in the given slice 881 // contains a request with the given method and whose URL matches the given regexp. 882 func assertOneRequestMatches(c *gc.C, requests []*gwacl.X509Request, method string, urlPattern string) { 883 for _, request := range requests { 884 matched, err := regexp.MatchString(urlPattern, request.URL) 885 if err == nil && request.Method == method && matched { 886 return 887 } 888 } 889 c.Error(fmt.Sprintf("none of the requests matches: Method=%v, URL pattern=%v", method, urlPattern)) 890 } 891 892 func (s *environSuite) TestDestroyStopsAllInstances(c *gc.C) { 893 s.setServiceDeletionConcurrency(3) 894 env := makeEnviron(c) 895 s.setDummyStorage(c, env) 896 897 // Simulate 2 instances corresponding to two Azure services. 898 prefix := env.getEnvPrefix() 899 service1Name := prefix + "service1" 900 service1, service1Desc := makeAzureService(service1Name) 901 services := []*gwacl.HostedService{service1} 902 // The call to AllInstances() will return only one service (service1). 903 listInstancesResponses := getAzureServiceListResponse(c, []gwacl.HostedServiceDescriptor{*service1Desc}) 904 destroyResponses := buildDestroyAzureServiceResponses(c, services) 905 responses := append(listInstancesResponses, destroyResponses...) 906 cleanupResponses := getVnetAndAffinityGroupCleanupResponses(c) 907 responses = append(responses, cleanupResponses...) 908 requests := gwacl.PatchManagementAPIResponses(responses) 909 910 err := env.Destroy() 911 c.Check(err, gc.IsNil) 912 913 // One request to get the list of all the environment's instances. 914 // Then two requests per destroyed machine (one to fetch the 915 // service's information, one to delete it) and two requests to delete 916 // the Virtual Network and the Affinity Group. 917 c.Check((*requests), gc.HasLen, 1+len(services)*2+2) 918 c.Check((*requests)[0].Method, gc.Equals, "GET") 919 assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".*") 920 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1Name+".*") 921 } 922 923 func (*environSuite) TestGetInstance(c *gc.C) { 924 env := makeEnviron(c) 925 prefix := env.getEnvPrefix() 926 serviceName := prefix + "instance-name" 927 serviceDesc := gwacl.HostedServiceDescriptor{ServiceName: serviceName} 928 service := gwacl.HostedService{HostedServiceDescriptor: serviceDesc} 929 responses := getAzureServiceResponses(c, service) 930 gwacl.PatchManagementAPIResponses(responses) 931 932 instance, err := env.getInstance("serviceName") 933 c.Check(err, gc.IsNil) 934 935 c.Check(string(instance.Id()), gc.Equals, serviceName) 936 c.Check(instance, gc.FitsTypeOf, &azureInstance{}) 937 azInstance := instance.(*azureInstance) 938 c.Check(azInstance.environ, gc.Equals, env) 939 } 940 941 func (*environSuite) TestNewOSVirtualDisk(c *gc.C) { 942 env := makeEnviron(c) 943 sourceImageName := "source-image-name" 944 945 vhd := env.newOSDisk(sourceImageName) 946 947 mediaLinkUrl, err := url.Parse(vhd.MediaLink) 948 c.Check(err, gc.IsNil) 949 storageAccount := env.ecfg.storageAccountName() 950 c.Check(mediaLinkUrl.Host, gc.Equals, fmt.Sprintf("%s.blob.core.windows.net", storageAccount)) 951 c.Check(vhd.SourceImageName, gc.Equals, sourceImageName) 952 } 953 954 // mapInputEndpointsByPort takes a slice of input endpoints, and returns them 955 // as a map keyed by their (external) ports. This makes it easier to query 956 // individual endpoints from an array whose ordering you don't know. 957 // Multiple input endpoints for the same port are treated as an error. 958 func mapInputEndpointsByPort(c *gc.C, endpoints []gwacl.InputEndpoint) map[int]gwacl.InputEndpoint { 959 mapping := make(map[int]gwacl.InputEndpoint) 960 for _, endpoint := range endpoints { 961 _, have := mapping[endpoint.Port] 962 c.Assert(have, gc.Equals, false) 963 mapping[endpoint.Port] = endpoint 964 } 965 return mapping 966 } 967 968 func (*environSuite) TestNewRole(c *gc.C) { 969 env := makeEnviron(c) 970 size := "Large" 971 vhd := env.newOSDisk("source-image-name") 972 userData := "example-user-data" 973 hostname := "hostname" 974 975 role := env.newRole(size, vhd, userData, hostname) 976 977 configs := role.ConfigurationSets 978 linuxConfig := configs[0] 979 networkConfig := configs[1] 980 c.Check(linuxConfig.CustomData, gc.Equals, userData) 981 c.Check(linuxConfig.Hostname, gc.Equals, hostname) 982 c.Check(linuxConfig.Username, gc.Not(gc.Equals), "") 983 c.Check(linuxConfig.Password, gc.Not(gc.Equals), "") 984 c.Check(linuxConfig.DisableSSHPasswordAuthentication, gc.Equals, "true") 985 c.Check(role.RoleSize, gc.Equals, size) 986 c.Check(role.OSVirtualHardDisk[0], gc.Equals, *vhd) 987 988 endpoints := mapInputEndpointsByPort(c, *networkConfig.InputEndpoints) 989 990 // The network config contains an endpoint for ssh communication. 991 sshEndpoint, ok := endpoints[22] 992 c.Assert(ok, gc.Equals, true) 993 c.Check(sshEndpoint.LocalPort, gc.Equals, 22) 994 c.Check(sshEndpoint.Protocol, gc.Equals, "tcp") 995 996 // There's also an endpoint for the state (mongodb) port. 997 // TODO: Ought to have this only for state servers. 998 stateEndpoint, ok := endpoints[env.Config().StatePort()] 999 c.Assert(ok, gc.Equals, true) 1000 c.Check(stateEndpoint.LocalPort, gc.Equals, env.Config().StatePort()) 1001 c.Check(stateEndpoint.Protocol, gc.Equals, "tcp") 1002 1003 // And one for the API port. 1004 // TODO: Ought to have this only for API servers. 1005 apiEndpoint, ok := endpoints[env.Config().APIPort()] 1006 c.Assert(ok, gc.Equals, true) 1007 c.Check(apiEndpoint.LocalPort, gc.Equals, env.Config().APIPort()) 1008 c.Check(apiEndpoint.Protocol, gc.Equals, "tcp") 1009 } 1010 1011 func (*environSuite) TestNewDeployment(c *gc.C) { 1012 env := makeEnviron(c) 1013 deploymentName := "deployment-name" 1014 deploymentLabel := "deployment-label" 1015 virtualNetworkName := "virtual-network-name" 1016 vhd := env.newOSDisk("source-image-name") 1017 role := env.newRole("Small", vhd, "user-data", "hostname") 1018 1019 deployment := env.newDeployment(role, deploymentName, deploymentLabel, virtualNetworkName) 1020 1021 base64Label := base64.StdEncoding.EncodeToString([]byte(deploymentLabel)) 1022 c.Check(deployment.Label, gc.Equals, base64Label) 1023 c.Check(deployment.Name, gc.Equals, deploymentName) 1024 c.Check(deployment.RoleList, gc.HasLen, 1) 1025 } 1026 1027 func (*environSuite) TestProviderReturnsAzureEnvironProvider(c *gc.C) { 1028 prov := makeEnviron(c).Provider() 1029 c.Assert(prov, gc.NotNil) 1030 azprov, ok := prov.(azureEnvironProvider) 1031 c.Assert(ok, gc.Equals, true) 1032 c.Check(azprov, gc.NotNil) 1033 } 1034 1035 func (*environSuite) TestCreateVirtualNetwork(c *gc.C) { 1036 env := makeEnviron(c) 1037 responses := []gwacl.DispatcherResponse{ 1038 // No existing configuration found. 1039 gwacl.NewDispatcherResponse(nil, http.StatusNotFound, nil), 1040 // Accept upload of new configuration. 1041 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 1042 } 1043 requests := gwacl.PatchManagementAPIResponses(responses) 1044 1045 env.createVirtualNetwork() 1046 1047 c.Assert(*requests, gc.HasLen, 2) 1048 request := (*requests)[1] 1049 body := gwacl.NetworkConfiguration{} 1050 err := xml.Unmarshal(request.Payload, &body) 1051 c.Assert(err, gc.IsNil) 1052 networkConf := (*body.VirtualNetworkSites)[0] 1053 c.Check(networkConf.Name, gc.Equals, env.getVirtualNetworkName()) 1054 c.Check(networkConf.AffinityGroup, gc.Equals, env.getAffinityGroupName()) 1055 } 1056 1057 func (*environSuite) TestDestroyVirtualNetwork(c *gc.C) { 1058 env := makeEnviron(c) 1059 // Prepare a configuration with a single virtual network. 1060 existingConfig := &gwacl.NetworkConfiguration{ 1061 XMLNS: gwacl.XMLNS_NC, 1062 VirtualNetworkSites: &[]gwacl.VirtualNetworkSite{ 1063 {Name: env.getVirtualNetworkName()}, 1064 }, 1065 } 1066 body, err := existingConfig.Serialize() 1067 c.Assert(err, gc.IsNil) 1068 responses := []gwacl.DispatcherResponse{ 1069 // Return existing configuration. 1070 gwacl.NewDispatcherResponse([]byte(body), http.StatusOK, nil), 1071 // Accept upload of new configuration. 1072 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 1073 } 1074 requests := gwacl.PatchManagementAPIResponses(responses) 1075 1076 env.deleteVirtualNetwork() 1077 1078 c.Assert(*requests, gc.HasLen, 2) 1079 // One request to get the existing network configuration. 1080 getRequest := (*requests)[0] 1081 c.Check(getRequest.Method, gc.Equals, "GET") 1082 // One request to update the network configuration. 1083 putRequest := (*requests)[1] 1084 c.Check(putRequest.Method, gc.Equals, "PUT") 1085 newConfig := gwacl.NetworkConfiguration{} 1086 err = xml.Unmarshal(putRequest.Payload, &newConfig) 1087 c.Assert(err, gc.IsNil) 1088 // The new configuration has no VirtualNetworkSites. 1089 c.Check(newConfig.VirtualNetworkSites, gc.IsNil) 1090 } 1091 1092 func (*environSuite) TestGetVirtualNetworkNameContainsEnvName(c *gc.C) { 1093 env := makeEnviron(c) 1094 c.Check(strings.Contains(env.getVirtualNetworkName(), env.Name()), jc.IsTrue) 1095 } 1096 1097 func (*environSuite) TestGetVirtualNetworkNameIsConstant(c *gc.C) { 1098 env := makeEnviron(c) 1099 c.Check(env.getVirtualNetworkName(), gc.Equals, env.getVirtualNetworkName()) 1100 } 1101 1102 func (*environSuite) TestCreateAffinityGroup(c *gc.C) { 1103 env := makeEnviron(c) 1104 responses := []gwacl.DispatcherResponse{ 1105 gwacl.NewDispatcherResponse(nil, http.StatusCreated, nil), 1106 } 1107 requests := gwacl.PatchManagementAPIResponses(responses) 1108 1109 env.createAffinityGroup() 1110 1111 c.Assert(*requests, gc.HasLen, 1) 1112 request := (*requests)[0] 1113 body := gwacl.CreateAffinityGroup{} 1114 err := xml.Unmarshal(request.Payload, &body) 1115 c.Assert(err, gc.IsNil) 1116 c.Check(body.Name, gc.Equals, env.getAffinityGroupName()) 1117 // This is a testing antipattern, the expected data comes from 1118 // config defaults. Fix it sometime. 1119 c.Check(body.Location, gc.Equals, "location") 1120 } 1121 1122 func (*environSuite) TestDestroyAffinityGroup(c *gc.C) { 1123 env := makeEnviron(c) 1124 responses := []gwacl.DispatcherResponse{ 1125 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), 1126 } 1127 requests := gwacl.PatchManagementAPIResponses(responses) 1128 1129 env.deleteAffinityGroup() 1130 1131 c.Assert(*requests, gc.HasLen, 1) 1132 request := (*requests)[0] 1133 c.Check(strings.Contains(request.URL, env.getAffinityGroupName()), jc.IsTrue) 1134 c.Check(request.Method, gc.Equals, "DELETE") 1135 } 1136 1137 func (*environSuite) TestGetAffinityGroupName(c *gc.C) { 1138 env := makeEnviron(c) 1139 c.Check(strings.Contains(env.getAffinityGroupName(), env.Name()), jc.IsTrue) 1140 } 1141 1142 func (*environSuite) TestGetAffinityGroupNameIsConstant(c *gc.C) { 1143 env := makeEnviron(c) 1144 c.Check(env.getAffinityGroupName(), gc.Equals, env.getAffinityGroupName()) 1145 } 1146 1147 func (*environSuite) TestGetImageMetadataSigningRequiredDefaultsToTrue(c *gc.C) { 1148 env := makeEnviron(c) 1149 // Hard-coded to true for now. Once we support other base URLs, this 1150 // may have to become configurable. 1151 c.Check(env.getImageMetadataSigningRequired(), gc.Equals, true) 1152 } 1153 1154 func (*environSuite) TestSelectInstanceTypeAndImageUsesForcedImage(c *gc.C) { 1155 env := makeEnviron(c) 1156 forcedImage := "my-image" 1157 env.ecfg.attrs["force-image-name"] = forcedImage 1158 1159 aim := gwacl.RoleNameMap["ExtraLarge"] 1160 cons := constraints.Value{ 1161 CpuCores: &aim.CpuCores, 1162 Mem: &aim.Mem, 1163 } 1164 1165 instanceType, image, err := env.selectInstanceTypeAndImage(cons, "precise", "West US") 1166 c.Assert(err, gc.IsNil) 1167 1168 c.Check(instanceType, gc.Equals, aim.Name) 1169 c.Check(image, gc.Equals, forcedImage) 1170 } 1171 1172 func (*environSuite) TestSelectInstanceTypeAndImageUsesSimplestreamsByDefault(c *gc.C) { 1173 env := makeEnviron(c) 1174 1175 // We'll tailor our constraints so as to get a specific instance type. 1176 aim := gwacl.RoleNameMap["ExtraSmall"] 1177 cons := constraints.Value{ 1178 CpuCores: &aim.CpuCores, 1179 Mem: &aim.Mem, 1180 } 1181 1182 // We have one image available. 1183 images := []*imagemetadata.ImageMetadata{ 1184 { 1185 Id: "image", 1186 VType: "Hyper-V", 1187 Arch: "amd64", 1188 RegionAlias: "North Europe", 1189 RegionName: "North Europe", 1190 Endpoint: "http://localhost/", 1191 }, 1192 } 1193 cleanup := patchFetchImageMetadata(images, nil) 1194 defer cleanup() 1195 1196 instanceType, image, err := env.selectInstanceTypeAndImage(cons, "precise", "West US") 1197 c.Assert(err, gc.IsNil) 1198 1199 c.Check(instanceType, gc.Equals, aim.Name) 1200 c.Check(image, gc.Equals, "image") 1201 } 1202 1203 func (*environSuite) TestConvertToInstances(c *gc.C) { 1204 services := []gwacl.HostedServiceDescriptor{ 1205 {ServiceName: "foo"}, {ServiceName: "bar"}, 1206 } 1207 env := makeEnviron(c) 1208 instances := convertToInstances(services, env) 1209 c.Check(instances, gc.DeepEquals, []instance.Instance{ 1210 &azureInstance{services[0], env}, 1211 &azureInstance{services[1], env}, 1212 }) 1213 } 1214 1215 func (*environSuite) TestExtractStorageKeyPicksPrimaryKeyIfSet(c *gc.C) { 1216 keys := gwacl.StorageAccountKeys{ 1217 Primary: "mainkey", 1218 Secondary: "otherkey", 1219 } 1220 c.Check(extractStorageKey(&keys), gc.Equals, "mainkey") 1221 } 1222 1223 func (*environSuite) TestExtractStorageKeyFallsBackToSecondaryKey(c *gc.C) { 1224 keys := gwacl.StorageAccountKeys{ 1225 Secondary: "sparekey", 1226 } 1227 c.Check(extractStorageKey(&keys), gc.Equals, "sparekey") 1228 } 1229 1230 func (*environSuite) TestExtractStorageKeyReturnsBlankIfNoneSet(c *gc.C) { 1231 c.Check(extractStorageKey(&gwacl.StorageAccountKeys{}), gc.Equals, "") 1232 } 1233 1234 func assertSourceContents(c *gc.C, source simplestreams.DataSource, filename string, content []byte) { 1235 rc, _, err := source.Fetch(filename) 1236 c.Assert(err, gc.IsNil) 1237 defer rc.Close() 1238 retrieved, err := ioutil.ReadAll(rc) 1239 c.Assert(err, gc.IsNil) 1240 c.Assert(retrieved, gc.DeepEquals, content) 1241 } 1242 1243 func (s *environSuite) assertGetImageMetadataSources(c *gc.C, stream, officialSourcePath string) { 1244 envAttrs := makeAzureConfigMap(c) 1245 if stream != "" { 1246 envAttrs["image-stream"] = stream 1247 } 1248 env := makeEnvironWithConfig(c, envAttrs) 1249 s.setDummyStorage(c, env) 1250 1251 data := []byte{1, 2, 3, 4} 1252 env.Storage().Put("images/filename", bytes.NewReader(data), int64(len(data))) 1253 1254 sources, err := imagemetadata.GetMetadataSources(env) 1255 c.Assert(err, gc.IsNil) 1256 c.Assert(len(sources), gc.Equals, 2) 1257 assertSourceContents(c, sources[0], "filename", data) 1258 url, err := sources[1].URL("") 1259 c.Assert(err, gc.IsNil) 1260 c.Assert(url, gc.Equals, fmt.Sprintf("http://cloud-images.ubuntu.com/%s/", officialSourcePath)) 1261 } 1262 1263 func (s *environSuite) TestGetImageMetadataSources(c *gc.C) { 1264 s.assertGetImageMetadataSources(c, "", "releases") 1265 s.assertGetImageMetadataSources(c, "released", "releases") 1266 s.assertGetImageMetadataSources(c, "daily", "daily") 1267 } 1268 1269 func (s *environSuite) TestGetToolsMetadataSources(c *gc.C) { 1270 env := makeEnviron(c) 1271 s.setDummyStorage(c, env) 1272 1273 data := []byte{1, 2, 3, 4} 1274 env.Storage().Put("tools/filename", bytes.NewReader(data), int64(len(data))) 1275 1276 sources, err := tools.GetMetadataSources(env) 1277 c.Assert(err, gc.IsNil) 1278 c.Assert(len(sources), gc.Equals, 1) 1279 assertSourceContents(c, sources[0], "filename", data) 1280 }