github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/cmd/jujud/bootstrap_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  
    12  	"github.com/juju/errors"
    13  	jc "github.com/juju/testing/checkers"
    14  	"github.com/juju/utils"
    15  	gc "launchpad.net/gocheck"
    16  	"launchpad.net/goyaml"
    17  
    18  	"github.com/juju/juju/agent"
    19  	"github.com/juju/juju/cmd"
    20  	"github.com/juju/juju/constraints"
    21  	"github.com/juju/juju/environs"
    22  	"github.com/juju/juju/environs/config"
    23  	"github.com/juju/juju/environs/configstore"
    24  	envtesting "github.com/juju/juju/environs/testing"
    25  	"github.com/juju/juju/instance"
    26  	jujutesting "github.com/juju/juju/juju/testing"
    27  	"github.com/juju/juju/provider/dummy"
    28  	"github.com/juju/juju/state"
    29  	"github.com/juju/juju/state/api/params"
    30  	"github.com/juju/juju/testing"
    31  	"github.com/juju/juju/version"
    32  	"github.com/juju/juju/worker/peergrouper"
    33  )
    34  
    35  var _ = configstore.Default
    36  
    37  // We don't want to use JujuConnSuite because it gives us
    38  // an already-bootstrapped environment.
    39  type BootstrapSuite struct {
    40  	testing.BaseSuite
    41  	testing.MgoSuite
    42  	envcfg          string
    43  	instanceId      instance.Id
    44  	dataDir         string
    45  	logDir          string
    46  	fakeEnsureMongo fakeEnsure
    47  	bootstrapName   string
    48  }
    49  
    50  var _ = gc.Suite(&BootstrapSuite{})
    51  
    52  type fakeEnsure struct {
    53  	ensureCount    int
    54  	initiateCount  int
    55  	dataDir        string
    56  	namespace      string
    57  	withHA         bool
    58  	info           params.StateServingInfo
    59  	initiateParams peergrouper.InitiateMongoParams
    60  	err            error
    61  }
    62  
    63  func (f *fakeEnsure) fakeEnsureMongo(dataDir, namespace string, info params.StateServingInfo, withHA bool) error {
    64  	f.ensureCount++
    65  	f.dataDir, f.namespace, f.info, f.withHA = dataDir, namespace, info, withHA
    66  	return f.err
    67  }
    68  
    69  func (f *fakeEnsure) fakeInitiateMongo(p peergrouper.InitiateMongoParams) error {
    70  	f.initiateCount++
    71  	f.initiateParams = p
    72  	return nil
    73  }
    74  
    75  func (s *BootstrapSuite) SetUpSuite(c *gc.C) {
    76  	s.PatchValue(&ensureMongoServer, s.fakeEnsureMongo.fakeEnsureMongo)
    77  	s.PatchValue(&maybeInitiateMongoServer, s.fakeEnsureMongo.fakeInitiateMongo)
    78  
    79  	s.BaseSuite.SetUpSuite(c)
    80  	s.MgoSuite.SetUpSuite(c)
    81  	s.makeTestEnv(c)
    82  }
    83  
    84  func (s *BootstrapSuite) TearDownSuite(c *gc.C) {
    85  	s.MgoSuite.TearDownSuite(c)
    86  	s.BaseSuite.TearDownSuite(c)
    87  	dummy.Reset()
    88  }
    89  
    90  func (s *BootstrapSuite) SetUpTest(c *gc.C) {
    91  	s.BaseSuite.SetUpTest(c)
    92  	s.MgoSuite.SetUpTest(c)
    93  	s.dataDir = c.MkDir()
    94  	s.logDir = c.MkDir()
    95  	s.fakeEnsureMongo = fakeEnsure{}
    96  }
    97  
    98  func (s *BootstrapSuite) TearDownTest(c *gc.C) {
    99  	s.MgoSuite.TearDownTest(c)
   100  	s.BaseSuite.TearDownTest(c)
   101  }
   102  
   103  var testPassword = "my-admin-secret"
   104  
   105  func testPasswordHash() string {
   106  	return utils.UserPasswordHash(testPassword, utils.CompatSalt)
   107  }
   108  
   109  func (s *BootstrapSuite) initBootstrapCommand(c *gc.C, jobs []params.MachineJob, args ...string) (machineConf agent.ConfigSetterWriter, cmd *BootstrapCommand, err error) {
   110  	if len(jobs) == 0 {
   111  		// Add default jobs.
   112  		jobs = []params.MachineJob{
   113  			params.JobManageEnviron, params.JobHostUnits,
   114  		}
   115  	}
   116  	// NOTE: the old test used an equivalent of the NewAgentConfig, but it
   117  	// really should be using NewStateMachineConfig.
   118  	agentParams := agent.AgentConfigParams{
   119  		LogDir:            s.logDir,
   120  		DataDir:           s.dataDir,
   121  		Jobs:              jobs,
   122  		Tag:               "machine-0",
   123  		UpgradedToVersion: version.Current.Number,
   124  		Password:          testPasswordHash(),
   125  		Nonce:             state.BootstrapNonce,
   126  		StateAddresses:    []string{testing.MgoServer.Addr()},
   127  		APIAddresses:      []string{"0.1.2.3:1234"},
   128  		CACert:            testing.CACert,
   129  		Values:            map[string]string{agent.Namespace: "foobar"},
   130  	}
   131  	servingInfo := params.StateServingInfo{
   132  		Cert:       "some cert",
   133  		PrivateKey: "some key",
   134  		APIPort:    3737,
   135  		StatePort:  testing.MgoServer.Port(),
   136  	}
   137  
   138  	machineConf, err = agent.NewStateMachineConfig(agentParams, servingInfo)
   139  	c.Assert(err, gc.IsNil)
   140  	err = machineConf.Write()
   141  	c.Assert(err, gc.IsNil)
   142  
   143  	cmd = &BootstrapCommand{}
   144  
   145  	err = testing.InitCommand(cmd, append([]string{"--data-dir", s.dataDir}, args...))
   146  	return machineConf, cmd, err
   147  }
   148  
   149  func (s *BootstrapSuite) TestInitializeEnvironment(c *gc.C) {
   150  	hw := instance.MustParseHardware("arch=amd64 mem=8G")
   151  	machConf, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", s.envcfg, "--instance-id", string(s.instanceId), "--hardware", hw.String())
   152  	c.Assert(err, gc.IsNil)
   153  	err = cmd.Run(nil)
   154  	c.Assert(err, gc.IsNil)
   155  
   156  	c.Assert(s.fakeEnsureMongo.dataDir, gc.Equals, s.dataDir)
   157  	c.Assert(s.fakeEnsureMongo.initiateCount, gc.Equals, 1)
   158  	c.Assert(s.fakeEnsureMongo.ensureCount, gc.Equals, 1)
   159  	c.Assert(s.fakeEnsureMongo.dataDir, gc.Equals, s.dataDir)
   160  	c.Assert(s.fakeEnsureMongo.withHA, jc.IsTrue)
   161  
   162  	expectInfo, exists := machConf.StateServingInfo()
   163  	c.Assert(exists, jc.IsTrue)
   164  	c.Assert(expectInfo.SharedSecret, gc.Equals, "")
   165  
   166  	servingInfo := s.fakeEnsureMongo.info
   167  	c.Assert(len(servingInfo.SharedSecret), gc.Not(gc.Equals), 0)
   168  	servingInfo.SharedSecret = ""
   169  	c.Assert(servingInfo, jc.DeepEquals, expectInfo)
   170  	expectDialAddrs := []string{fmt.Sprintf("127.0.0.1:%d", expectInfo.StatePort)}
   171  	gotDialAddrs := s.fakeEnsureMongo.initiateParams.DialInfo.Addrs
   172  	c.Assert(gotDialAddrs, gc.DeepEquals, expectDialAddrs)
   173  
   174  	memberHost := fmt.Sprintf("%s:%d", s.bootstrapName, expectInfo.StatePort)
   175  	c.Assert(s.fakeEnsureMongo.initiateParams.MemberHostPort, gc.Equals, memberHost)
   176  	c.Assert(s.fakeEnsureMongo.initiateParams.User, gc.Equals, "")
   177  	c.Assert(s.fakeEnsureMongo.initiateParams.Password, gc.Equals, "")
   178  
   179  	st, err := state.Open(&state.Info{
   180  		Addrs:    []string{testing.MgoServer.Addr()},
   181  		CACert:   testing.CACert,
   182  		Password: testPasswordHash(),
   183  	}, state.DefaultDialOpts(), environs.NewStatePolicy())
   184  	c.Assert(err, gc.IsNil)
   185  	defer st.Close()
   186  	machines, err := st.AllMachines()
   187  	c.Assert(err, gc.IsNil)
   188  	c.Assert(machines, gc.HasLen, 1)
   189  
   190  	instid, err := machines[0].InstanceId()
   191  	c.Assert(err, gc.IsNil)
   192  	c.Assert(instid, gc.Equals, instance.Id(string(s.instanceId)))
   193  
   194  	stateHw, err := machines[0].HardwareCharacteristics()
   195  	c.Assert(err, gc.IsNil)
   196  	c.Assert(stateHw, gc.NotNil)
   197  	c.Assert(*stateHw, gc.DeepEquals, hw)
   198  
   199  	cons, err := st.EnvironConstraints()
   200  	c.Assert(err, gc.IsNil)
   201  	c.Assert(&cons, jc.Satisfies, constraints.IsEmpty)
   202  }
   203  
   204  func (s *BootstrapSuite) TestSetConstraints(c *gc.C) {
   205  	tcons := constraints.Value{Mem: uint64p(2048), CpuCores: uint64p(2)}
   206  	_, cmd, err := s.initBootstrapCommand(c, nil,
   207  		"--env-config", s.envcfg,
   208  		"--instance-id", string(s.instanceId),
   209  		"--constraints", tcons.String(),
   210  	)
   211  	c.Assert(err, gc.IsNil)
   212  	err = cmd.Run(nil)
   213  	c.Assert(err, gc.IsNil)
   214  
   215  	st, err := state.Open(&state.Info{
   216  		Addrs:    []string{testing.MgoServer.Addr()},
   217  		CACert:   testing.CACert,
   218  		Password: testPasswordHash(),
   219  	}, state.DefaultDialOpts(), environs.NewStatePolicy())
   220  	c.Assert(err, gc.IsNil)
   221  	defer st.Close()
   222  	cons, err := st.EnvironConstraints()
   223  	c.Assert(err, gc.IsNil)
   224  	c.Assert(cons, gc.DeepEquals, tcons)
   225  
   226  	machines, err := st.AllMachines()
   227  	c.Assert(err, gc.IsNil)
   228  	c.Assert(machines, gc.HasLen, 1)
   229  	cons, err = machines[0].Constraints()
   230  	c.Assert(err, gc.IsNil)
   231  	c.Assert(cons, gc.DeepEquals, tcons)
   232  }
   233  
   234  func uint64p(v uint64) *uint64 {
   235  	return &v
   236  }
   237  
   238  func (s *BootstrapSuite) TestDefaultMachineJobs(c *gc.C) {
   239  	expectedJobs := []state.MachineJob{
   240  		state.JobManageEnviron, state.JobHostUnits,
   241  	}
   242  	_, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", s.envcfg, "--instance-id", string(s.instanceId))
   243  	c.Assert(err, gc.IsNil)
   244  	err = cmd.Run(nil)
   245  	c.Assert(err, gc.IsNil)
   246  
   247  	st, err := state.Open(&state.Info{
   248  		Addrs:    []string{testing.MgoServer.Addr()},
   249  		CACert:   testing.CACert,
   250  		Password: testPasswordHash(),
   251  	}, state.DefaultDialOpts(), environs.NewStatePolicy())
   252  	c.Assert(err, gc.IsNil)
   253  	defer st.Close()
   254  	m, err := st.Machine("0")
   255  	c.Assert(err, gc.IsNil)
   256  	c.Assert(m.Jobs(), gc.DeepEquals, expectedJobs)
   257  }
   258  
   259  func (s *BootstrapSuite) TestConfiguredMachineJobs(c *gc.C) {
   260  	jobs := []params.MachineJob{params.JobManageEnviron}
   261  	_, cmd, err := s.initBootstrapCommand(c, jobs, "--env-config", s.envcfg, "--instance-id", string(s.instanceId))
   262  	c.Assert(err, gc.IsNil)
   263  	err = cmd.Run(nil)
   264  	c.Assert(err, gc.IsNil)
   265  
   266  	st, err := state.Open(&state.Info{
   267  		Addrs:    []string{testing.MgoServer.Addr()},
   268  		CACert:   testing.CACert,
   269  		Password: testPasswordHash(),
   270  	}, state.DefaultDialOpts(), environs.NewStatePolicy())
   271  	c.Assert(err, gc.IsNil)
   272  	defer st.Close()
   273  	m, err := st.Machine("0")
   274  	c.Assert(err, gc.IsNil)
   275  	c.Assert(m.Jobs(), gc.DeepEquals, []state.MachineJob{state.JobManageEnviron})
   276  }
   277  
   278  func testOpenState(c *gc.C, info *state.Info, expectErrType error) {
   279  	st, err := state.Open(info, state.DefaultDialOpts(), environs.NewStatePolicy())
   280  	if st != nil {
   281  		st.Close()
   282  	}
   283  	if expectErrType != nil {
   284  		c.Assert(err, gc.FitsTypeOf, expectErrType)
   285  	} else {
   286  		c.Assert(err, gc.IsNil)
   287  	}
   288  }
   289  
   290  func (s *BootstrapSuite) TestInitialPassword(c *gc.C) {
   291  	machineConf, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", s.envcfg, "--instance-id", string(s.instanceId))
   292  	c.Assert(err, gc.IsNil)
   293  
   294  	err = cmd.Run(nil)
   295  	c.Assert(err, gc.IsNil)
   296  
   297  	// Check that we cannot now connect to the state without a
   298  	// password.
   299  	info := &state.Info{
   300  		Addrs:  []string{testing.MgoServer.Addr()},
   301  		CACert: testing.CACert,
   302  	}
   303  	testOpenState(c, info, errors.Unauthorizedf(""))
   304  
   305  	// Check we can log in to mongo as admin.
   306  	info.Tag, info.Password = "", testPasswordHash()
   307  	st, err := state.Open(info, state.DefaultDialOpts(), environs.NewStatePolicy())
   308  	c.Assert(err, gc.IsNil)
   309  	// Reset password so the tests can continue to use the same server.
   310  	defer st.Close()
   311  	defer st.SetAdminMongoPassword("")
   312  
   313  	// Check that the admin user has been given an appropriate
   314  	// password
   315  	u, err := st.User("admin")
   316  	c.Assert(err, gc.IsNil)
   317  	c.Assert(u.PasswordValid(testPassword), gc.Equals, true)
   318  
   319  	// Check that the machine configuration has been given a new
   320  	// password and that we can connect to mongo as that machine
   321  	// and that the in-mongo password also verifies correctly.
   322  	machineConf1, err := agent.ReadConfig(agent.ConfigPath(machineConf.DataDir(), "machine-0"))
   323  	c.Assert(err, gc.IsNil)
   324  
   325  	stateinfo, ok := machineConf1.StateInfo()
   326  	c.Assert(ok, jc.IsTrue)
   327  	st, err = state.Open(stateinfo, state.DialOpts{}, environs.NewStatePolicy())
   328  	c.Assert(err, gc.IsNil)
   329  	defer st.Close()
   330  
   331  	m, err := st.Machine("0")
   332  	c.Assert(err, gc.IsNil)
   333  	c.Assert(m.HasVote(), jc.IsTrue)
   334  }
   335  
   336  var bootstrapArgTests = []struct {
   337  	input              []string
   338  	err                string
   339  	expectedInstanceId string
   340  	expectedHardware   instance.HardwareCharacteristics
   341  	expectedConfig     map[string]interface{}
   342  }{
   343  	{
   344  		// no value supplied for env-config
   345  		err: "--env-config option must be set",
   346  	}, {
   347  		// empty env-config
   348  		input: []string{"--env-config", ""},
   349  		err:   "--env-config option must be set",
   350  	}, {
   351  		// wrong, should be base64
   352  		input: []string{"--env-config", "name: banana\n"},
   353  		err:   ".*illegal base64 data at input byte.*",
   354  	}, {
   355  		// no value supplied for instance-id
   356  		input: []string{
   357  			"--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")),
   358  		},
   359  		err: "--instance-id option must be set",
   360  	}, {
   361  		// empty instance-id
   362  		input: []string{
   363  			"--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")),
   364  			"--instance-id", "",
   365  		},
   366  		err: "--instance-id option must be set",
   367  	}, {
   368  		input: []string{
   369  			"--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")),
   370  			"--instance-id", "anything",
   371  		},
   372  		expectedInstanceId: "anything",
   373  		expectedConfig:     map[string]interface{}{"name": "banana"},
   374  	}, {
   375  		input: []string{
   376  			"--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")),
   377  			"--instance-id", "anything",
   378  			"--hardware", "nonsense",
   379  		},
   380  		err: `invalid value "nonsense" for flag --hardware: malformed characteristic "nonsense"`,
   381  	}, {
   382  		input: []string{
   383  			"--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")),
   384  			"--instance-id", "anything",
   385  			"--hardware", "arch=amd64 cpu-cores=4 root-disk=2T",
   386  		},
   387  		expectedInstanceId: "anything",
   388  		expectedHardware:   instance.MustParseHardware("arch=amd64 cpu-cores=4 root-disk=2T"),
   389  		expectedConfig:     map[string]interface{}{"name": "banana"},
   390  	},
   391  }
   392  
   393  func (s *BootstrapSuite) TestBootstrapArgs(c *gc.C) {
   394  	for i, t := range bootstrapArgTests {
   395  		c.Logf("test %d", i)
   396  		var args []string
   397  		args = append(args, t.input...)
   398  		_, cmd, err := s.initBootstrapCommand(c, nil, args...)
   399  		if t.err == "" {
   400  			c.Assert(cmd, gc.NotNil)
   401  			c.Assert(err, gc.IsNil)
   402  			c.Assert(cmd.EnvConfig, gc.DeepEquals, t.expectedConfig)
   403  			c.Assert(cmd.InstanceId, gc.Equals, t.expectedInstanceId)
   404  			c.Assert(cmd.Hardware, gc.DeepEquals, t.expectedHardware)
   405  		} else {
   406  			c.Assert(err, gc.ErrorMatches, t.err)
   407  		}
   408  	}
   409  }
   410  
   411  func (s *BootstrapSuite) makeTestEnv(c *gc.C) {
   412  	attrs := dummy.SampleConfig().Merge(
   413  		testing.Attrs{
   414  			"agent-version": version.Current.Number.String(),
   415  		},
   416  	).Delete("admin-secret", "ca-private-key")
   417  
   418  	cfg, err := config.New(config.NoDefaults, attrs)
   419  	c.Assert(err, gc.IsNil)
   420  	provider, err := environs.Provider(cfg.Type())
   421  	c.Assert(err, gc.IsNil)
   422  	env, err := provider.Prepare(nullContext(), cfg)
   423  	c.Assert(err, gc.IsNil)
   424  
   425  	envtesting.MustUploadFakeTools(env.Storage())
   426  	inst, _, _, err := jujutesting.StartInstance(env, "0")
   427  	c.Assert(err, gc.IsNil)
   428  	s.instanceId = inst.Id()
   429  
   430  	addresses, err := inst.Addresses()
   431  	c.Assert(err, gc.IsNil)
   432  	s.bootstrapName = instance.SelectPublicAddress(addresses)
   433  	s.envcfg = b64yaml(env.Config().AllAttrs()).encode()
   434  }
   435  
   436  func nullContext() *cmd.Context {
   437  	ctx, _ := cmd.DefaultContext()
   438  	ctx.Stdin = io.LimitReader(nil, 0)
   439  	ctx.Stdout = ioutil.Discard
   440  	ctx.Stderr = ioutil.Discard
   441  	return ctx
   442  }
   443  
   444  type b64yaml map[string]interface{}
   445  
   446  func (m b64yaml) encode() string {
   447  	data, err := goyaml.Marshal(m)
   448  	if err != nil {
   449  		panic(err)
   450  	}
   451  	return base64.StdEncoding.EncodeToString(data)
   452  }