launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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 bootstrapContext(c *gc.C) environs.BootstrapContext {
   153  	return envtesting.NewBootstrapContext(coretesting.Context(c))
   154  }
   155  
   156  func (suite *environSuite) TestStartInstanceStartsInstance(c *gc.C) {
   157  	suite.setupFakeTools(c)
   158  	env := suite.makeEnviron()
   159  	// Create node 0: it will be used as the bootstrap node.
   160  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   161  	err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{})
   162  	c.Assert(err, gc.IsNil)
   163  	// The bootstrap node has been acquired and started.
   164  	operations := suite.testMAASObject.TestServer.NodeOperations()
   165  	actions, found := operations["node0"]
   166  	c.Check(found, gc.Equals, true)
   167  	c.Check(actions, gc.DeepEquals, []string{"acquire", "start"})
   168  
   169  	// Test the instance id is correctly recorded for the bootstrap node.
   170  	// Check that the state holds the id of the bootstrap machine.
   171  	stateData, err := bootstrap.LoadState(env.Storage())
   172  	c.Assert(err, gc.IsNil)
   173  	c.Assert(stateData.StateInstances, gc.HasLen, 1)
   174  	insts, err := env.AllInstances()
   175  	c.Assert(err, gc.IsNil)
   176  	c.Assert(insts, gc.HasLen, 1)
   177  	c.Check(insts[0].Id(), gc.Equals, stateData.StateInstances[0])
   178  
   179  	// Create node 1: it will be used as instance number 1.
   180  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node1", "hostname": "host1"}`)
   181  	// TODO(wallyworld) - test instance metadata
   182  	instance, _ := testing.AssertStartInstance(c, env, "1")
   183  	c.Assert(err, gc.IsNil)
   184  	c.Check(instance, gc.NotNil)
   185  
   186  	// The instance number 1 has been acquired and started.
   187  	actions, found = operations["node1"]
   188  	c.Assert(found, gc.Equals, true)
   189  	c.Check(actions, gc.DeepEquals, []string{"acquire", "start"})
   190  
   191  	// The value of the "user data" parameter used when starting the node
   192  	// contains the run cmd used to write the machine information onto
   193  	// the node's filesystem.
   194  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   195  	nodeRequestValues, found := requestValues["node1"]
   196  	c.Assert(found, gc.Equals, true)
   197  	c.Assert(len(nodeRequestValues), gc.Equals, 2)
   198  	userData := nodeRequestValues[1].Get("user_data")
   199  	decodedUserData, err := decodeUserData(userData)
   200  	c.Assert(err, gc.IsNil)
   201  	info := machineInfo{"host1"}
   202  	cloudinitRunCmd, err := info.cloudinitRunCmd()
   203  	c.Assert(err, gc.IsNil)
   204  	data, err := goyaml.Marshal(cloudinitRunCmd)
   205  	c.Assert(err, gc.IsNil)
   206  	c.Check(string(decodedUserData), gc.Matches, "(.|\n)*"+string(data)+"(\n|.)*")
   207  
   208  	// Trash the tools and try to start another instance.
   209  	envtesting.RemoveTools(c, env.Storage())
   210  	instance, _, err = testing.StartInstance(env, "2")
   211  	c.Check(instance, gc.IsNil)
   212  	c.Check(err, jc.Satisfies, errors.IsNotFoundError)
   213  }
   214  
   215  func uint64p(val uint64) *uint64 {
   216  	return &val
   217  }
   218  
   219  func stringp(val string) *string {
   220  	return &val
   221  }
   222  
   223  func (suite *environSuite) TestAcquireNode(c *gc.C) {
   224  	stor := NewStorage(suite.makeEnviron())
   225  	fakeTools := envtesting.MustUploadFakeToolsVersions(stor, version.Current)[0]
   226  	env := suite.makeEnviron()
   227  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   228  
   229  	_, _, err := env.acquireNode(constraints.Value{}, tools.List{fakeTools})
   230  
   231  	c.Check(err, gc.IsNil)
   232  	operations := suite.testMAASObject.TestServer.NodeOperations()
   233  	actions, found := operations["node0"]
   234  	c.Assert(found, gc.Equals, true)
   235  	c.Check(actions, gc.DeepEquals, []string{"acquire"})
   236  }
   237  
   238  func (suite *environSuite) TestAcquireNodeTakesConstraintsIntoAccount(c *gc.C) {
   239  	stor := NewStorage(suite.makeEnviron())
   240  	fakeTools := envtesting.MustUploadFakeToolsVersions(stor, version.Current)[0]
   241  	env := suite.makeEnviron()
   242  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   243  	constraints := constraints.Value{Arch: stringp("arm"), Mem: uint64p(1024)}
   244  
   245  	_, _, err := env.acquireNode(constraints, tools.List{fakeTools})
   246  
   247  	c.Check(err, gc.IsNil)
   248  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   249  	nodeRequestValues, found := requestValues["node0"]
   250  	c.Assert(found, gc.Equals, true)
   251  	c.Assert(nodeRequestValues[0].Get("arch"), gc.Equals, "arm")
   252  	c.Assert(nodeRequestValues[0].Get("mem"), gc.Equals, "1024")
   253  }
   254  
   255  func (suite *environSuite) TestAcquireNodePassedAgentName(c *gc.C) {
   256  	stor := NewStorage(suite.makeEnviron())
   257  	fakeTools := envtesting.MustUploadFakeToolsVersions(stor, version.Current)[0]
   258  	env := suite.makeEnviron()
   259  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   260  
   261  	_, _, err := env.acquireNode(constraints.Value{}, tools.List{fakeTools})
   262  
   263  	c.Check(err, gc.IsNil)
   264  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   265  	nodeRequestValues, found := requestValues["node0"]
   266  	c.Assert(found, gc.Equals, true)
   267  	c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, exampleAgentName)
   268  }
   269  
   270  func (*environSuite) TestConvertConstraints(c *gc.C) {
   271  	var testValues = []struct {
   272  		constraints    constraints.Value
   273  		expectedResult url.Values
   274  	}{
   275  		{constraints.Value{Arch: stringp("arm")}, url.Values{"arch": {"arm"}}},
   276  		{constraints.Value{CpuCores: uint64p(4)}, url.Values{"cpu_count": {"4"}}},
   277  		{constraints.Value{Mem: uint64p(1024)}, url.Values{"mem": {"1024"}}},
   278  		// CpuPower is ignored.
   279  		{constraints.Value{CpuPower: uint64p(1024)}, url.Values{}},
   280  		// RootDisk is ignored.
   281  		{constraints.Value{RootDisk: uint64p(8192)}, url.Values{}},
   282  		{constraints.Value{Tags: &[]string{"foo", "bar"}}, url.Values{"tags": {"foo,bar"}}},
   283  		{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"}}},
   284  	}
   285  	for _, test := range testValues {
   286  		c.Check(convertConstraints(test.constraints), gc.DeepEquals, test.expectedResult)
   287  	}
   288  }
   289  
   290  func (suite *environSuite) getInstance(systemId string) *maasInstance {
   291  	input := `{"system_id": "` + systemId + `"}`
   292  	node := suite.testMAASObject.TestServer.NewNode(input)
   293  	return &maasInstance{maasObject: &node, environ: suite.makeEnviron()}
   294  }
   295  
   296  func (suite *environSuite) TestStopInstancesReturnsIfParameterEmpty(c *gc.C) {
   297  	suite.getInstance("test1")
   298  
   299  	err := suite.makeEnviron().StopInstances([]instance.Instance{})
   300  	c.Check(err, gc.IsNil)
   301  	operations := suite.testMAASObject.TestServer.NodeOperations()
   302  	c.Check(operations, gc.DeepEquals, map[string][]string{})
   303  }
   304  
   305  func (suite *environSuite) TestStopInstancesStopsAndReleasesInstances(c *gc.C) {
   306  	instance1 := suite.getInstance("test1")
   307  	instance2 := suite.getInstance("test2")
   308  	suite.getInstance("test3")
   309  	instances := []instance.Instance{instance1, instance2}
   310  
   311  	err := suite.makeEnviron().StopInstances(instances)
   312  
   313  	c.Check(err, gc.IsNil)
   314  	operations := suite.testMAASObject.TestServer.NodeOperations()
   315  	expectedOperations := map[string][]string{"test1": {"release"}, "test2": {"release"}}
   316  	c.Check(operations, gc.DeepEquals, expectedOperations)
   317  }
   318  
   319  func (suite *environSuite) TestStateInfo(c *gc.C) {
   320  	env := suite.makeEnviron()
   321  	hostname := "test"
   322  	input := `{"system_id": "system_id", "hostname": "` + hostname + `"}`
   323  	node := suite.testMAASObject.TestServer.NewNode(input)
   324  	testInstance := &maasInstance{maasObject: &node, environ: suite.makeEnviron()}
   325  	err := bootstrap.SaveState(
   326  		env.Storage(),
   327  		&bootstrap.BootstrapState{StateInstances: []instance.Id{testInstance.Id()}})
   328  	c.Assert(err, gc.IsNil)
   329  
   330  	stateInfo, apiInfo, err := env.StateInfo()
   331  	c.Assert(err, gc.IsNil)
   332  
   333  	cfg := env.Config()
   334  	statePortSuffix := fmt.Sprintf(":%d", cfg.StatePort())
   335  	apiPortSuffix := fmt.Sprintf(":%d", cfg.APIPort())
   336  	c.Assert(stateInfo.Addrs, gc.DeepEquals, []string{hostname + statePortSuffix})
   337  	c.Assert(apiInfo.Addrs, gc.DeepEquals, []string{hostname + apiPortSuffix})
   338  }
   339  
   340  func (suite *environSuite) TestStateInfoFailsIfNoStateInstances(c *gc.C) {
   341  	env := suite.makeEnviron()
   342  
   343  	_, _, err := env.StateInfo()
   344  
   345  	c.Check(err, gc.Equals, environs.ErrNotBootstrapped)
   346  }
   347  
   348  func (suite *environSuite) TestDestroy(c *gc.C) {
   349  	env := suite.makeEnviron()
   350  	suite.getInstance("test1")
   351  	data := makeRandomBytes(10)
   352  	suite.testMAASObject.TestServer.NewFile("filename", data)
   353  	stor := env.Storage()
   354  
   355  	err := env.Destroy()
   356  	c.Check(err, gc.IsNil)
   357  
   358  	// Instances have been stopped.
   359  	operations := suite.testMAASObject.TestServer.NodeOperations()
   360  	expectedOperations := map[string][]string{"test1": {"release"}}
   361  	c.Check(operations, gc.DeepEquals, expectedOperations)
   362  	// Files have been cleaned up.
   363  	listing, err := storage.List(stor, "")
   364  	c.Assert(err, gc.IsNil)
   365  	c.Check(listing, gc.DeepEquals, []string{})
   366  }
   367  
   368  // It would be nice if we could unit-test Bootstrap() in more detail, but
   369  // at the time of writing that would require more support from gomaasapi's
   370  // testing service than we have.
   371  func (suite *environSuite) TestBootstrapSucceeds(c *gc.C) {
   372  	suite.setupFakeTools(c)
   373  	env := suite.makeEnviron()
   374  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "thenode", "hostname": "host"}`)
   375  	err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{})
   376  	c.Assert(err, gc.IsNil)
   377  }
   378  
   379  func (suite *environSuite) TestBootstrapFailsIfNoTools(c *gc.C) {
   380  	suite.setupFakeTools(c)
   381  	env := suite.makeEnviron()
   382  	// Can't RemoveAllTools, no public storage.
   383  	envtesting.RemoveTools(c, env.Storage())
   384  	// Disable auto-uploading by setting the agent version.
   385  	cfg, err := env.Config().Apply(map[string]interface{}{
   386  		"agent-version": version.Current.Number.String(),
   387  	})
   388  	c.Assert(err, gc.IsNil)
   389  	err = env.SetConfig(cfg)
   390  	c.Assert(err, gc.IsNil)
   391  	err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{})
   392  	c.Check(err, gc.ErrorMatches, "cannot find bootstrap tools.*")
   393  }
   394  
   395  func (suite *environSuite) TestBootstrapFailsIfNoNodes(c *gc.C) {
   396  	suite.setupFakeTools(c)
   397  	env := suite.makeEnviron()
   398  	err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{})
   399  	// Since there are no nodes, the attempt to allocate one returns a
   400  	// 409: Conflict.
   401  	c.Check(err, gc.ErrorMatches, ".*409.*")
   402  }
   403  
   404  func (suite *environSuite) TestBootstrapIntegratesWithEnvirons(c *gc.C) {
   405  	suite.setupFakeTools(c)
   406  	env := suite.makeEnviron()
   407  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "bootstrapnode", "hostname": "host"}`)
   408  
   409  	// bootstrap.Bootstrap calls Environ.Bootstrap.  This works.
   410  	err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{})
   411  	c.Assert(err, gc.IsNil)
   412  }
   413  
   414  func assertSourceContents(c *gc.C, source simplestreams.DataSource, filename string, content []byte) {
   415  	rc, _, err := source.Fetch(filename)
   416  	c.Assert(err, gc.IsNil)
   417  	defer rc.Close()
   418  	retrieved, err := ioutil.ReadAll(rc)
   419  	c.Assert(err, gc.IsNil)
   420  	c.Assert(retrieved, gc.DeepEquals, content)
   421  }
   422  
   423  func (suite *environSuite) assertGetImageMetadataSources(c *gc.C, stream, officialSourcePath string) {
   424  	// Make an env configured with the stream.
   425  	testAttrs := maasEnvAttrs
   426  	testAttrs = testAttrs.Merge(coretesting.Attrs{
   427  		"maas-server": suite.testMAASObject.TestServer.URL,
   428  	})
   429  	if stream != "" {
   430  		testAttrs = testAttrs.Merge(coretesting.Attrs{
   431  			"image-stream": stream,
   432  		})
   433  	}
   434  	attrs := coretesting.FakeConfig().Merge(testAttrs)
   435  	cfg, err := config.New(config.NoDefaults, attrs)
   436  	c.Assert(err, gc.IsNil)
   437  	env, err := NewEnviron(cfg)
   438  	c.Assert(err, gc.IsNil)
   439  
   440  	// Add a dummy file to storage so we can use that to check the
   441  	// obtained source later.
   442  	data := makeRandomBytes(10)
   443  	stor := NewStorage(env)
   444  	err = stor.Put("images/filename", bytes.NewBuffer([]byte(data)), int64(len(data)))
   445  	c.Assert(err, gc.IsNil)
   446  	sources, err := imagemetadata.GetMetadataSources(env)
   447  	c.Assert(err, gc.IsNil)
   448  	c.Assert(len(sources), gc.Equals, 2)
   449  	assertSourceContents(c, sources[0], "filename", data)
   450  	url, err := sources[1].URL("")
   451  	c.Assert(err, gc.IsNil)
   452  	c.Assert(url, gc.Equals, fmt.Sprintf("http://cloud-images.ubuntu.com/%s/", officialSourcePath))
   453  }
   454  
   455  func (suite *environSuite) TestGetImageMetadataSources(c *gc.C) {
   456  	suite.assertGetImageMetadataSources(c, "", "releases")
   457  	suite.assertGetImageMetadataSources(c, "released", "releases")
   458  	suite.assertGetImageMetadataSources(c, "daily", "daily")
   459  }
   460  
   461  func (suite *environSuite) TestGetToolsMetadataSources(c *gc.C) {
   462  	env := suite.makeEnviron()
   463  	// Add a dummy file to storage so we can use that to check the
   464  	// obtained source later.
   465  	data := makeRandomBytes(10)
   466  	stor := NewStorage(env)
   467  	err := stor.Put("tools/filename", bytes.NewBuffer([]byte(data)), int64(len(data)))
   468  	c.Assert(err, gc.IsNil)
   469  	sources, err := envtools.GetMetadataSources(env)
   470  	c.Assert(err, gc.IsNil)
   471  	c.Assert(len(sources), gc.Equals, 1)
   472  	assertSourceContents(c, sources[0], "filename", data)
   473  }