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  }