github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/provider/maas/environ_whitebox_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package maas 5 6 import ( 7 "bytes" 8 "encoding/base64" 9 "fmt" 10 "io/ioutil" 11 "net/url" 12 13 gc "launchpad.net/gocheck" 14 "launchpad.net/goyaml" 15 16 "launchpad.net/juju-core/constraints" 17 "launchpad.net/juju-core/environs" 18 "launchpad.net/juju-core/environs/bootstrap" 19 "launchpad.net/juju-core/environs/config" 20 "launchpad.net/juju-core/environs/imagemetadata" 21 "launchpad.net/juju-core/environs/simplestreams" 22 "launchpad.net/juju-core/environs/storage" 23 envtesting "launchpad.net/juju-core/environs/testing" 24 envtools "launchpad.net/juju-core/environs/tools" 25 "launchpad.net/juju-core/errors" 26 "launchpad.net/juju-core/instance" 27 "launchpad.net/juju-core/juju/testing" 28 coretesting "launchpad.net/juju-core/testing" 29 jc "launchpad.net/juju-core/testing/checkers" 30 "launchpad.net/juju-core/tools" 31 "launchpad.net/juju-core/utils" 32 "launchpad.net/juju-core/version" 33 ) 34 35 type environSuite struct { 36 providerSuite 37 } 38 39 const ( 40 allocatedNode = `{"system_id": "test-allocated"}` 41 ) 42 43 var _ = gc.Suite(&environSuite{}) 44 45 // getTestConfig creates a customized sample MAAS provider configuration. 46 func getTestConfig(name, server, oauth, secret string) *config.Config { 47 ecfg, err := newConfig(map[string]interface{}{ 48 "name": name, 49 "maas-server": server, 50 "maas-oauth": oauth, 51 "admin-secret": secret, 52 "authorized-keys": "I-am-not-a-real-key", 53 }) 54 if err != nil { 55 panic(err) 56 } 57 return ecfg.Config 58 } 59 60 func (suite *environSuite) setupFakeProviderStateFile(c *gc.C) { 61 suite.testMAASObject.TestServer.NewFile(bootstrap.StateFile, []byte("test file content")) 62 } 63 64 func (suite *environSuite) setupFakeTools(c *gc.C) { 65 stor := NewStorage(suite.makeEnviron()) 66 envtesting.UploadFakeTools(c, stor) 67 } 68 69 func (suite *environSuite) addNode(jsonText string) instance.Id { 70 node := suite.testMAASObject.TestServer.NewNode(jsonText) 71 resourceURI, _ := node.GetField("resource_uri") 72 return instance.Id(resourceURI) 73 } 74 75 func (suite *environSuite) TestInstancesReturnsInstances(c *gc.C) { 76 id := suite.addNode(allocatedNode) 77 instances, err := suite.makeEnviron().Instances([]instance.Id{id}) 78 79 c.Check(err, gc.IsNil) 80 c.Assert(instances, gc.HasLen, 1) 81 c.Assert(instances[0].Id(), gc.Equals, id) 82 } 83 84 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfEmptyParameter(c *gc.C) { 85 suite.addNode(allocatedNode) 86 instances, err := suite.makeEnviron().Instances([]instance.Id{}) 87 88 c.Check(err, gc.Equals, environs.ErrNoInstances) 89 c.Check(instances, gc.IsNil) 90 } 91 92 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNilParameter(c *gc.C) { 93 suite.addNode(allocatedNode) 94 instances, err := suite.makeEnviron().Instances(nil) 95 96 c.Check(err, gc.Equals, environs.ErrNoInstances) 97 c.Check(instances, gc.IsNil) 98 } 99 100 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNoneFound(c *gc.C) { 101 instances, err := suite.makeEnviron().Instances([]instance.Id{"unknown"}) 102 c.Check(err, gc.Equals, environs.ErrNoInstances) 103 c.Check(instances, gc.IsNil) 104 } 105 106 func (suite *environSuite) TestAllInstances(c *gc.C) { 107 id := suite.addNode(allocatedNode) 108 instances, err := suite.makeEnviron().AllInstances() 109 110 c.Check(err, gc.IsNil) 111 c.Assert(instances, gc.HasLen, 1) 112 c.Assert(instances[0].Id(), gc.Equals, id) 113 } 114 115 func (suite *environSuite) TestAllInstancesReturnsEmptySliceIfNoInstance(c *gc.C) { 116 instances, err := suite.makeEnviron().AllInstances() 117 118 c.Check(err, gc.IsNil) 119 c.Check(instances, gc.HasLen, 0) 120 } 121 122 func (suite *environSuite) TestInstancesReturnsErrorIfPartialInstances(c *gc.C) { 123 known := suite.addNode(allocatedNode) 124 suite.addNode(`{"system_id": "test2"}`) 125 unknown := instance.Id("unknown systemID") 126 instances, err := suite.makeEnviron().Instances([]instance.Id{known, unknown}) 127 128 c.Check(err, gc.Equals, environs.ErrPartialInstances) 129 c.Assert(instances, gc.HasLen, 2) 130 c.Check(instances[0].Id(), gc.Equals, known) 131 c.Check(instances[1], gc.IsNil) 132 } 133 134 func (suite *environSuite) TestStorageReturnsStorage(c *gc.C) { 135 env := suite.makeEnviron() 136 stor := env.Storage() 137 c.Check(stor, gc.NotNil) 138 // The Storage object is really a maasStorage. 139 specificStorage := stor.(*maasStorage) 140 // Its environment pointer refers back to its environment. 141 c.Check(specificStorage.environUnlocked, gc.Equals, env) 142 } 143 144 func decodeUserData(userData string) ([]byte, error) { 145 data, err := base64.StdEncoding.DecodeString(userData) 146 if err != nil { 147 return []byte(""), err 148 } 149 return utils.Gunzip(data) 150 } 151 152 func (suite *environSuite) TestStartInstanceStartsInstance(c *gc.C) { 153 suite.setupFakeTools(c) 154 env := suite.makeEnviron() 155 // Create node 0: it will be used as the bootstrap node. 156 suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) 157 err := bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) 158 c.Assert(err, gc.IsNil) 159 // The bootstrap node has been acquired and started. 160 operations := suite.testMAASObject.TestServer.NodeOperations() 161 actions, found := operations["node0"] 162 c.Check(found, gc.Equals, true) 163 c.Check(actions, gc.DeepEquals, []string{"acquire", "start"}) 164 165 // Test the instance id is correctly recorded for the bootstrap node. 166 // Check that the state holds the id of the bootstrap machine. 167 stateData, err := bootstrap.LoadState(env.Storage()) 168 c.Assert(err, gc.IsNil) 169 c.Assert(stateData.StateInstances, gc.HasLen, 1) 170 insts, err := env.AllInstances() 171 c.Assert(err, gc.IsNil) 172 c.Assert(insts, gc.HasLen, 1) 173 c.Check(insts[0].Id(), gc.Equals, stateData.StateInstances[0]) 174 175 // Create node 1: it will be used as instance number 1. 176 suite.testMAASObject.TestServer.NewNode(`{"system_id": "node1", "hostname": "host1"}`) 177 // TODO(wallyworld) - test instance metadata 178 instance, _ := testing.AssertStartInstance(c, env, "1") 179 c.Assert(err, gc.IsNil) 180 c.Check(instance, gc.NotNil) 181 182 // The instance number 1 has been acquired and started. 183 actions, found = operations["node1"] 184 c.Assert(found, gc.Equals, true) 185 c.Check(actions, gc.DeepEquals, []string{"acquire", "start"}) 186 187 // The value of the "user data" parameter used when starting the node 188 // contains the run cmd used to write the machine information onto 189 // the node's filesystem. 190 requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues() 191 nodeRequestValues, found := requestValues["node1"] 192 c.Assert(found, gc.Equals, true) 193 c.Assert(len(nodeRequestValues), gc.Equals, 2) 194 userData := nodeRequestValues[1].Get("user_data") 195 decodedUserData, err := decodeUserData(userData) 196 c.Assert(err, gc.IsNil) 197 info := machineInfo{"host1"} 198 cloudinitRunCmd, err := info.cloudinitRunCmd() 199 c.Assert(err, gc.IsNil) 200 data, err := goyaml.Marshal(cloudinitRunCmd) 201 c.Assert(err, gc.IsNil) 202 c.Check(string(decodedUserData), gc.Matches, "(.|\n)*"+string(data)+"(\n|.)*") 203 204 // Trash the tools and try to start another instance. 205 envtesting.RemoveTools(c, env.Storage()) 206 instance, _, err = testing.StartInstance(env, "2") 207 c.Check(instance, gc.IsNil) 208 c.Check(err, jc.Satisfies, errors.IsNotFoundError) 209 } 210 211 func uint64p(val uint64) *uint64 { 212 return &val 213 } 214 215 func stringp(val string) *string { 216 return &val 217 } 218 219 func (suite *environSuite) TestAcquireNode(c *gc.C) { 220 stor := NewStorage(suite.makeEnviron()) 221 fakeTools := envtesting.MustUploadFakeToolsVersions(stor, version.Current)[0] 222 env := suite.makeEnviron() 223 suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) 224 225 _, _, err := env.acquireNode(constraints.Value{}, tools.List{fakeTools}) 226 227 c.Check(err, gc.IsNil) 228 operations := suite.testMAASObject.TestServer.NodeOperations() 229 actions, found := operations["node0"] 230 c.Assert(found, gc.Equals, true) 231 c.Check(actions, gc.DeepEquals, []string{"acquire"}) 232 } 233 234 func (suite *environSuite) TestAcquireNodeTakesConstraintsIntoAccount(c *gc.C) { 235 stor := NewStorage(suite.makeEnviron()) 236 fakeTools := envtesting.MustUploadFakeToolsVersions(stor, version.Current)[0] 237 env := suite.makeEnviron() 238 suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) 239 constraints := constraints.Value{Arch: stringp("arm"), Mem: uint64p(1024)} 240 241 _, _, err := env.acquireNode(constraints, tools.List{fakeTools}) 242 243 c.Check(err, gc.IsNil) 244 requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues() 245 nodeRequestValues, found := requestValues["node0"] 246 c.Assert(found, gc.Equals, true) 247 c.Assert(nodeRequestValues[0].Get("arch"), gc.Equals, "arm") 248 c.Assert(nodeRequestValues[0].Get("mem"), gc.Equals, "1024") 249 } 250 251 func (suite *environSuite) TestAcquireNodePassedAgentName(c *gc.C) { 252 stor := NewStorage(suite.makeEnviron()) 253 fakeTools := envtesting.MustUploadFakeToolsVersions(stor, version.Current)[0] 254 env := suite.makeEnviron() 255 suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) 256 257 _, _, err := env.acquireNode(constraints.Value{}, tools.List{fakeTools}) 258 259 c.Check(err, gc.IsNil) 260 requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues() 261 nodeRequestValues, found := requestValues["node0"] 262 c.Assert(found, gc.Equals, true) 263 c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, exampleAgentName) 264 } 265 266 func (*environSuite) TestConvertConstraints(c *gc.C) { 267 var testValues = []struct { 268 constraints constraints.Value 269 expectedResult url.Values 270 }{ 271 {constraints.Value{Arch: stringp("arm")}, url.Values{"arch": {"arm"}}}, 272 {constraints.Value{CpuCores: uint64p(4)}, url.Values{"cpu_count": {"4"}}}, 273 {constraints.Value{Mem: uint64p(1024)}, url.Values{"mem": {"1024"}}}, 274 // CpuPower is ignored. 275 {constraints.Value{CpuPower: uint64p(1024)}, url.Values{}}, 276 // RootDisk is ignored. 277 {constraints.Value{RootDisk: uint64p(8192)}, url.Values{}}, 278 {constraints.Value{Tags: &[]string{"foo", "bar"}}, url.Values{"tags": {"foo,bar"}}}, 279 {constraints.Value{Arch: stringp("arm"), CpuCores: uint64p(4), Mem: uint64p(1024), CpuPower: uint64p(1024), RootDisk: uint64p(8192), Tags: &[]string{"foo", "bar"}}, url.Values{"arch": {"arm"}, "cpu_count": {"4"}, "mem": {"1024"}, "tags": {"foo,bar"}}}, 280 } 281 for _, test := range testValues { 282 c.Check(convertConstraints(test.constraints), gc.DeepEquals, test.expectedResult) 283 } 284 } 285 286 func (suite *environSuite) getInstance(systemId string) *maasInstance { 287 input := `{"system_id": "` + systemId + `"}` 288 node := suite.testMAASObject.TestServer.NewNode(input) 289 return &maasInstance{maasObject: &node, environ: suite.makeEnviron()} 290 } 291 292 func (suite *environSuite) TestStopInstancesReturnsIfParameterEmpty(c *gc.C) { 293 suite.getInstance("test1") 294 295 err := suite.makeEnviron().StopInstances([]instance.Instance{}) 296 c.Check(err, gc.IsNil) 297 operations := suite.testMAASObject.TestServer.NodeOperations() 298 c.Check(operations, gc.DeepEquals, map[string][]string{}) 299 } 300 301 func (suite *environSuite) TestStopInstancesStopsAndReleasesInstances(c *gc.C) { 302 instance1 := suite.getInstance("test1") 303 instance2 := suite.getInstance("test2") 304 suite.getInstance("test3") 305 instances := []instance.Instance{instance1, instance2} 306 307 err := suite.makeEnviron().StopInstances(instances) 308 309 c.Check(err, gc.IsNil) 310 operations := suite.testMAASObject.TestServer.NodeOperations() 311 expectedOperations := map[string][]string{"test1": {"release"}, "test2": {"release"}} 312 c.Check(operations, gc.DeepEquals, expectedOperations) 313 } 314 315 func (suite *environSuite) TestStateInfo(c *gc.C) { 316 env := suite.makeEnviron() 317 hostname := "test" 318 input := `{"system_id": "system_id", "hostname": "` + hostname + `"}` 319 node := suite.testMAASObject.TestServer.NewNode(input) 320 testInstance := &maasInstance{maasObject: &node, environ: suite.makeEnviron()} 321 err := bootstrap.SaveState( 322 env.Storage(), 323 &bootstrap.BootstrapState{StateInstances: []instance.Id{testInstance.Id()}}) 324 c.Assert(err, gc.IsNil) 325 326 stateInfo, apiInfo, err := env.StateInfo() 327 c.Assert(err, gc.IsNil) 328 329 cfg := env.Config() 330 statePortSuffix := fmt.Sprintf(":%d", cfg.StatePort()) 331 apiPortSuffix := fmt.Sprintf(":%d", cfg.APIPort()) 332 c.Assert(stateInfo.Addrs, gc.DeepEquals, []string{hostname + statePortSuffix}) 333 c.Assert(apiInfo.Addrs, gc.DeepEquals, []string{hostname + apiPortSuffix}) 334 } 335 336 func (suite *environSuite) TestStateInfoFailsIfNoStateInstances(c *gc.C) { 337 env := suite.makeEnviron() 338 339 _, _, err := env.StateInfo() 340 341 c.Check(err, gc.Equals, environs.ErrNotBootstrapped) 342 } 343 344 func (suite *environSuite) TestDestroy(c *gc.C) { 345 env := suite.makeEnviron() 346 suite.getInstance("test1") 347 data := makeRandomBytes(10) 348 suite.testMAASObject.TestServer.NewFile("filename", data) 349 stor := env.Storage() 350 351 err := env.Destroy() 352 c.Check(err, gc.IsNil) 353 354 // Instances have been stopped. 355 operations := suite.testMAASObject.TestServer.NodeOperations() 356 expectedOperations := map[string][]string{"test1": {"release"}} 357 c.Check(operations, gc.DeepEquals, expectedOperations) 358 // Files have been cleaned up. 359 listing, err := storage.List(stor, "") 360 c.Assert(err, gc.IsNil) 361 c.Check(listing, gc.DeepEquals, []string{}) 362 } 363 364 // It would be nice if we could unit-test Bootstrap() in more detail, but 365 // at the time of writing that would require more support from gomaasapi's 366 // testing service than we have. 367 func (suite *environSuite) TestBootstrapSucceeds(c *gc.C) { 368 suite.setupFakeTools(c) 369 env := suite.makeEnviron() 370 suite.testMAASObject.TestServer.NewNode(`{"system_id": "thenode", "hostname": "host"}`) 371 err := bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) 372 c.Assert(err, gc.IsNil) 373 } 374 375 func (suite *environSuite) TestBootstrapFailsIfNoTools(c *gc.C) { 376 suite.setupFakeTools(c) 377 env := suite.makeEnviron() 378 // Can't RemoveAllTools, no public storage. 379 envtesting.RemoveTools(c, env.Storage()) 380 // Disable auto-uploading by setting the agent version. 381 cfg, err := env.Config().Apply(map[string]interface{}{ 382 "agent-version": version.Current.Number.String(), 383 }) 384 c.Assert(err, gc.IsNil) 385 err = env.SetConfig(cfg) 386 c.Assert(err, gc.IsNil) 387 err = bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) 388 c.Check(err, gc.ErrorMatches, "cannot find bootstrap tools.*") 389 } 390 391 func (suite *environSuite) TestBootstrapFailsIfNoNodes(c *gc.C) { 392 suite.setupFakeTools(c) 393 env := suite.makeEnviron() 394 err := bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) 395 // Since there are no nodes, the attempt to allocate one returns a 396 // 409: Conflict. 397 c.Check(err, gc.ErrorMatches, ".*409.*") 398 } 399 400 func (suite *environSuite) TestBootstrapIntegratesWithEnvirons(c *gc.C) { 401 suite.setupFakeTools(c) 402 env := suite.makeEnviron() 403 suite.testMAASObject.TestServer.NewNode(`{"system_id": "bootstrapnode", "hostname": "host"}`) 404 405 // bootstrap.Bootstrap calls Environ.Bootstrap. This works. 406 err := bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) 407 c.Assert(err, gc.IsNil) 408 } 409 410 func assertSourceContents(c *gc.C, source simplestreams.DataSource, filename string, content []byte) { 411 rc, _, err := source.Fetch(filename) 412 c.Assert(err, gc.IsNil) 413 defer rc.Close() 414 retrieved, err := ioutil.ReadAll(rc) 415 c.Assert(err, gc.IsNil) 416 c.Assert(retrieved, gc.DeepEquals, content) 417 } 418 419 func (suite *environSuite) assertGetImageMetadataSources(c *gc.C, stream, officialSourcePath string) { 420 // Make an env configured with the stream. 421 testAttrs := maasEnvAttrs 422 testAttrs = testAttrs.Merge(coretesting.Attrs{ 423 "maas-server": suite.testMAASObject.TestServer.URL, 424 }) 425 if stream != "" { 426 testAttrs = testAttrs.Merge(coretesting.Attrs{ 427 "image-stream": stream, 428 }) 429 } 430 attrs := coretesting.FakeConfig().Merge(testAttrs) 431 cfg, err := config.New(config.NoDefaults, attrs) 432 c.Assert(err, gc.IsNil) 433 env, err := NewEnviron(cfg) 434 c.Assert(err, gc.IsNil) 435 436 // Add a dummy file to storage so we can use that to check the 437 // obtained source later. 438 data := makeRandomBytes(10) 439 stor := NewStorage(env) 440 err = stor.Put("images/filename", bytes.NewBuffer([]byte(data)), int64(len(data))) 441 c.Assert(err, gc.IsNil) 442 sources, err := imagemetadata.GetMetadataSources(env) 443 c.Assert(err, gc.IsNil) 444 c.Assert(len(sources), gc.Equals, 2) 445 assertSourceContents(c, sources[0], "filename", data) 446 url, err := sources[1].URL("") 447 c.Assert(err, gc.IsNil) 448 c.Assert(url, gc.Equals, fmt.Sprintf("http://cloud-images.ubuntu.com/%s/", officialSourcePath)) 449 } 450 451 func (suite *environSuite) TestGetImageMetadataSources(c *gc.C) { 452 suite.assertGetImageMetadataSources(c, "", "releases") 453 suite.assertGetImageMetadataSources(c, "released", "releases") 454 suite.assertGetImageMetadataSources(c, "daily", "daily") 455 } 456 457 func (suite *environSuite) TestGetToolsMetadataSources(c *gc.C) { 458 env := suite.makeEnviron() 459 // Add a dummy file to storage so we can use that to check the 460 // obtained source later. 461 data := makeRandomBytes(10) 462 stor := NewStorage(env) 463 err := stor.Put("tools/filename", bytes.NewBuffer([]byte(data)), int64(len(data))) 464 c.Assert(err, gc.IsNil) 465 sources, err := envtools.GetMetadataSources(env) 466 c.Assert(err, gc.IsNil) 467 c.Assert(len(sources), gc.Equals, 1) 468 assertSourceContents(c, sources[0], "filename", data) 469 }