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