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 }