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 }