github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/juju/testing/conn.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package testing 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "strings" 12 "time" 13 14 "github.com/juju/errors" 15 "github.com/juju/names" 16 gitjujutesting "github.com/juju/testing" 17 jc "github.com/juju/testing/checkers" 18 "github.com/juju/utils" 19 gc "gopkg.in/check.v1" 20 "gopkg.in/juju/charm.v4" 21 goyaml "gopkg.in/yaml.v1" 22 23 "github.com/juju/juju/agent" 24 "github.com/juju/juju/api" 25 "github.com/juju/juju/cmd/envcmd" 26 "github.com/juju/juju/environs" 27 "github.com/juju/juju/environs/bootstrap" 28 "github.com/juju/juju/environs/config" 29 "github.com/juju/juju/environs/configstore" 30 "github.com/juju/juju/environs/filestorage" 31 "github.com/juju/juju/environs/storage" 32 envtesting "github.com/juju/juju/environs/testing" 33 "github.com/juju/juju/environs/tools" 34 "github.com/juju/juju/juju" 35 "github.com/juju/juju/juju/osenv" 36 "github.com/juju/juju/mongo" 37 "github.com/juju/juju/provider/dummy" 38 "github.com/juju/juju/state" 39 statestorage "github.com/juju/juju/state/storage" 40 "github.com/juju/juju/state/toolstorage" 41 "github.com/juju/juju/testcharms" 42 "github.com/juju/juju/testing" 43 "github.com/juju/juju/testing/factory" 44 "github.com/juju/juju/version" 45 ) 46 47 // JujuConnSuite provides a freshly bootstrapped juju.Conn 48 // for each test. It also includes testing.BaseSuite. 49 // 50 // It also sets up RootDir to point to a directory hierarchy 51 // mirroring the intended juju directory structure, including 52 // the following: 53 // RootDir/home/ubuntu/.juju/environments.yaml 54 // The dummy environments.yaml file, holding 55 // a default environment named "dummyenv" 56 // which uses the "dummy" environment type. 57 // RootDir/var/lib/juju 58 // An empty directory returned as DataDir - the 59 // root of the juju data storage space. 60 // $HOME is set to point to RootDir/home/ubuntu. 61 type JujuConnSuite struct { 62 // TODO: JujuConnSuite should not be concerned both with JUJU_HOME and with 63 // /var/lib/juju: the use cases are completely non-overlapping, and any tests that 64 // really do need both to exist ought to be embedding distinct fixtures for the 65 // distinct environments. 66 gitjujutesting.MgoSuite 67 testing.FakeJujuHomeSuite 68 envtesting.ToolsFixture 69 70 DefaultToolsStorageDir string 71 DefaultToolsStorage storage.Storage 72 73 State *state.State 74 Environ environs.Environ 75 APIState *api.State 76 apiStates []*api.State // additional api.States to close on teardown 77 ConfigStore configstore.Storage 78 BackingState *state.State // The State being used by the API server 79 RootDir string // The faked-up root directory. 80 LogDir string 81 oldHome string 82 oldJujuHome string 83 DummyConfig testing.Attrs 84 Factory *factory.Factory 85 } 86 87 const AdminSecret = "dummy-secret" 88 89 func (s *JujuConnSuite) SetUpSuite(c *gc.C) { 90 s.MgoSuite.SetUpSuite(c) 91 s.FakeJujuHomeSuite.SetUpSuite(c) 92 } 93 94 func (s *JujuConnSuite) TearDownSuite(c *gc.C) { 95 s.FakeJujuHomeSuite.TearDownSuite(c) 96 s.MgoSuite.TearDownSuite(c) 97 } 98 99 func (s *JujuConnSuite) SetUpTest(c *gc.C) { 100 s.MgoSuite.SetUpTest(c) 101 s.FakeJujuHomeSuite.SetUpTest(c) 102 s.ToolsFixture.SetUpTest(c) 103 s.PatchValue(&configstore.DefaultAdminUsername, dummy.AdminUserTag().Name()) 104 s.setUpConn(c) 105 s.Factory = factory.NewFactory(s.State) 106 } 107 108 func (s *JujuConnSuite) TearDownTest(c *gc.C) { 109 s.tearDownConn(c) 110 s.ToolsFixture.TearDownTest(c) 111 s.FakeJujuHomeSuite.TearDownTest(c) 112 s.MgoSuite.TearDownTest(c) 113 } 114 115 // Reset returns environment state to that which existed at the start of 116 // the test. 117 func (s *JujuConnSuite) Reset(c *gc.C) { 118 s.tearDownConn(c) 119 s.setUpConn(c) 120 } 121 122 func (s *JujuConnSuite) AdminUserTag(c *gc.C) names.UserTag { 123 env, err := s.State.StateServerEnvironment() 124 c.Assert(err, jc.ErrorIsNil) 125 return env.Owner() 126 } 127 128 func (s *JujuConnSuite) MongoInfo(c *gc.C) *mongo.MongoInfo { 129 info := s.State.MongoConnectionInfo() 130 info.Password = "dummy-secret" 131 return info 132 } 133 134 func (s *JujuConnSuite) APIInfo(c *gc.C) *api.Info { 135 apiInfo, err := environs.APIInfo(s.Environ) 136 c.Assert(err, jc.ErrorIsNil) 137 apiInfo.Tag = s.AdminUserTag(c) 138 apiInfo.Password = "dummy-secret" 139 apiInfo.EnvironTag = s.State.EnvironTag() 140 return apiInfo 141 } 142 143 // openAPIAs opens the API and ensures that the *api.State returned will be 144 // closed during the test teardown by using a cleanup function. 145 func (s *JujuConnSuite) openAPIAs(c *gc.C, tag names.Tag, password, nonce string) *api.State { 146 apiInfo := s.APIInfo(c) 147 apiInfo.Tag = tag 148 apiInfo.Password = password 149 apiInfo.Nonce = nonce 150 apiState, err := api.Open(apiInfo, api.DialOpts{}) 151 c.Assert(err, jc.ErrorIsNil) 152 c.Assert(apiState, gc.NotNil) 153 s.apiStates = append(s.apiStates, apiState) 154 return apiState 155 } 156 157 // OpenAPIAs opens the API using the given identity tag and password for 158 // authentication. The returned *api.State should not be closed by the caller 159 // as a cleanup function has been registered to do that. 160 func (s *JujuConnSuite) OpenAPIAs(c *gc.C, tag names.Tag, password string) *api.State { 161 return s.openAPIAs(c, tag, password, "") 162 } 163 164 // OpenAPIAsMachine opens the API using the given machine tag, password and 165 // nonce for authentication. The returned *api.State should not be closed by 166 // the caller as a cleanup function has been registered to do that. 167 func (s *JujuConnSuite) OpenAPIAsMachine(c *gc.C, tag names.Tag, password, nonce string) *api.State { 168 return s.openAPIAs(c, tag, password, nonce) 169 } 170 171 // OpenAPIAsNewMachine creates a new machine entry that lives in system state, 172 // and then uses that to open the API. The returned *api.State should not be 173 // closed by the caller as a cleanup function has been registered to do that. 174 // The machine will run the supplied jobs; if none are given, JobHostUnits is assumed. 175 func (s *JujuConnSuite) OpenAPIAsNewMachine(c *gc.C, jobs ...state.MachineJob) (*api.State, *state.Machine) { 176 if len(jobs) == 0 { 177 jobs = []state.MachineJob{state.JobHostUnits} 178 } 179 machine, err := s.State.AddMachine("quantal", jobs...) 180 c.Assert(err, jc.ErrorIsNil) 181 password, err := utils.RandomPassword() 182 c.Assert(err, jc.ErrorIsNil) 183 err = machine.SetPassword(password) 184 c.Assert(err, jc.ErrorIsNil) 185 err = machine.SetProvisioned("foo", "fake_nonce", nil) 186 c.Assert(err, jc.ErrorIsNil) 187 return s.openAPIAs(c, machine.Tag(), password, "fake_nonce"), machine 188 } 189 190 func PreferredDefaultVersions(conf *config.Config, template version.Binary) []version.Binary { 191 prefVersion := template 192 prefVersion.Series = config.PreferredSeries(conf) 193 defaultVersion := template 194 if prefVersion.Series != testing.FakeDefaultSeries { 195 defaultVersion.Series = testing.FakeDefaultSeries 196 } 197 return []version.Binary{prefVersion, defaultVersion} 198 } 199 200 func (s *JujuConnSuite) setUpConn(c *gc.C) { 201 if s.RootDir != "" { 202 panic("JujuConnSuite.setUpConn without teardown") 203 } 204 s.RootDir = c.MkDir() 205 s.oldHome = utils.Home() 206 home := filepath.Join(s.RootDir, "/home/ubuntu") 207 err := os.MkdirAll(home, 0777) 208 c.Assert(err, jc.ErrorIsNil) 209 utils.SetHome(home) 210 s.oldJujuHome = osenv.SetJujuHome(filepath.Join(home, ".juju")) 211 err = os.Mkdir(osenv.JujuHome(), 0777) 212 c.Assert(err, jc.ErrorIsNil) 213 214 err = os.MkdirAll(s.DataDir(), 0777) 215 c.Assert(err, jc.ErrorIsNil) 216 s.PatchEnvironment(osenv.JujuEnvEnvKey, "") 217 218 // TODO(rog) remove these files and add them only when 219 // the tests specifically need them (in cmd/juju for example) 220 s.writeSampleConfig(c, osenv.JujuHomePath("environments.yaml")) 221 222 err = ioutil.WriteFile(osenv.JujuHomePath("dummyenv-cert.pem"), []byte(testing.CACert), 0666) 223 c.Assert(err, jc.ErrorIsNil) 224 225 err = ioutil.WriteFile(osenv.JujuHomePath("dummyenv-private-key.pem"), []byte(testing.CAKey), 0600) 226 c.Assert(err, jc.ErrorIsNil) 227 228 store, err := configstore.Default() 229 c.Assert(err, jc.ErrorIsNil) 230 s.ConfigStore = store 231 232 ctx := testing.Context(c) 233 environ, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(ctx), s.ConfigStore) 234 c.Assert(err, jc.ErrorIsNil) 235 // sanity check we've got the correct environment. 236 c.Assert(environ.Config().Name(), gc.Equals, "dummyenv") 237 s.PatchValue(&dummy.DataDir, s.DataDir()) 238 s.LogDir = c.MkDir() 239 s.PatchValue(&dummy.LogDir, s.LogDir) 240 241 versions := PreferredDefaultVersions(environ.Config(), version.Binary{Number: version.Current.Number, Series: "precise", Arch: "amd64"}) 242 versions = append(versions, version.Current) 243 244 // Upload tools for both preferred and fake default series 245 s.DefaultToolsStorageDir = c.MkDir() 246 s.PatchValue(&tools.DefaultBaseURL, s.DefaultToolsStorageDir) 247 stor, err := filestorage.NewFileStorageWriter(s.DefaultToolsStorageDir) 248 c.Assert(err, jc.ErrorIsNil) 249 envtesting.AssertUploadFakeToolsVersions(c, stor, "released", "released", versions...) 250 s.DefaultToolsStorage = stor 251 252 err = bootstrap.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{}) 253 c.Assert(err, jc.ErrorIsNil) 254 255 s.BackingState = environ.(GetStater).GetStateInAPIServer() 256 257 s.State, err = newState(environ, s.BackingState.MongoConnectionInfo()) 258 c.Assert(err, jc.ErrorIsNil) 259 260 s.APIState, err = juju.NewAPIState(s.AdminUserTag(c), environ, api.DialOpts{}) 261 c.Assert(err, jc.ErrorIsNil) 262 263 err = s.State.SetAPIHostPorts(s.APIState.APIHostPorts()) 264 c.Assert(err, jc.ErrorIsNil) 265 266 s.Environ = environ 267 268 // Insert expected values... 269 servingInfo := state.StateServingInfo{ 270 PrivateKey: testing.ServerKey, 271 Cert: testing.ServerCert, 272 CAPrivateKey: testing.CAKey, 273 SharedSecret: "really, really secret", 274 APIPort: 4321, 275 StatePort: 1234, 276 } 277 s.State.SetStateServingInfo(servingInfo) 278 } 279 280 // AddToolsToState adds tools to tools storage. 281 func (s *JujuConnSuite) AddToolsToState(c *gc.C, versions ...version.Binary) { 282 stor, err := s.State.ToolsStorage() 283 c.Assert(err, jc.ErrorIsNil) 284 defer stor.Close() 285 for _, v := range versions { 286 content := v.String() 287 hash := fmt.Sprintf("sha256(%s)", content) 288 err := stor.AddTools(strings.NewReader(content), toolstorage.Metadata{ 289 Version: v, 290 Size: int64(len(content)), 291 SHA256: hash, 292 }) 293 c.Assert(err, jc.ErrorIsNil) 294 } 295 } 296 297 // AddDefaultToolsToState adds tools to tools storage for 298 // {Number: version.Current.Number, Arch: amd64}, for the 299 // "precise" series and the environment's preferred series. 300 // The preferred series is default-series if specified, 301 // otherwise the latest LTS. 302 func (s *JujuConnSuite) AddDefaultToolsToState(c *gc.C) { 303 preferredVersion := version.Current 304 preferredVersion.Arch = "amd64" 305 versions := PreferredDefaultVersions(s.Environ.Config(), preferredVersion) 306 versions = append(versions, version.Current) 307 s.AddToolsToState(c, versions...) 308 } 309 310 var redialStrategy = utils.AttemptStrategy{ 311 Total: 60 * time.Second, 312 Delay: 250 * time.Millisecond, 313 } 314 315 // newState returns a new State that uses the given environment. 316 // The environment must have already been bootstrapped. 317 func newState(environ environs.Environ, mongoInfo *mongo.MongoInfo) (*state.State, error) { 318 password := environ.Config().AdminSecret() 319 if password == "" { 320 return nil, fmt.Errorf("cannot connect without admin-secret") 321 } 322 323 mongoInfo.Password = password 324 opts := mongo.DefaultDialOpts() 325 st, err := state.Open(mongoInfo, opts, environs.NewStatePolicy()) 326 if errors.IsUnauthorized(err) { 327 // We try for a while because we might succeed in 328 // connecting to mongo before the state has been 329 // initialized and the initial password set. 330 for a := redialStrategy.Start(); a.Next(); { 331 st, err = state.Open(mongoInfo, opts, environs.NewStatePolicy()) 332 if !errors.IsUnauthorized(err) { 333 break 334 } 335 } 336 if err != nil { 337 return nil, err 338 } 339 } else if err != nil { 340 return nil, err 341 } 342 if err := updateSecrets(environ, st); err != nil { 343 st.Close() 344 return nil, fmt.Errorf("unable to push secrets: %v", err) 345 } 346 return st, nil 347 } 348 349 func updateSecrets(env environs.Environ, st *state.State) error { 350 secrets, err := env.Provider().SecretAttrs(env.Config()) 351 if err != nil { 352 return err 353 } 354 cfg, err := st.EnvironConfig() 355 if err != nil { 356 return err 357 } 358 secretAttrs := make(map[string]interface{}) 359 attrs := cfg.AllAttrs() 360 for k, v := range secrets { 361 if _, exists := attrs[k]; exists { 362 // Environment already has secrets. Won't send again. 363 return nil 364 } else { 365 secretAttrs[k] = v 366 } 367 } 368 return st.UpdateEnvironConfig(secretAttrs, nil, nil) 369 } 370 371 // PutCharm uploads the given charm to provider storage, and adds a 372 // state.Charm to the state. The charm is not uploaded if a charm with 373 // the same URL already exists in the state. 374 // If bumpRevision is true, the charm must be a local directory, 375 // and the revision number will be incremented before pushing. 376 func PutCharm(st *state.State, curl *charm.URL, repo charm.Repository, bumpRevision bool) (*state.Charm, error) { 377 if curl.Revision == -1 { 378 rev, err := charm.Latest(repo, curl) 379 if err != nil { 380 return nil, fmt.Errorf("cannot get latest charm revision: %v", err) 381 } 382 curl = curl.WithRevision(rev) 383 } 384 ch, err := repo.Get(curl) 385 if err != nil { 386 return nil, fmt.Errorf("cannot get charm: %v", err) 387 } 388 if bumpRevision { 389 chd, ok := ch.(*charm.CharmDir) 390 if !ok { 391 return nil, fmt.Errorf("cannot increment revision of charm %q: not a directory", curl) 392 } 393 if err = chd.SetDiskRevision(chd.Revision() + 1); err != nil { 394 return nil, fmt.Errorf("cannot increment revision of charm %q: %v", curl, err) 395 } 396 curl = curl.WithRevision(chd.Revision()) 397 } 398 if sch, err := st.Charm(curl); err == nil { 399 return sch, nil 400 } 401 return addCharm(st, curl, ch) 402 } 403 404 func addCharm(st *state.State, curl *charm.URL, ch charm.Charm) (*state.Charm, error) { 405 var f *os.File 406 name := charm.Quote(curl.String()) 407 switch ch := ch.(type) { 408 case *charm.CharmDir: 409 var err error 410 if f, err = ioutil.TempFile("", name); err != nil { 411 return nil, err 412 } 413 defer os.Remove(f.Name()) 414 defer f.Close() 415 err = ch.ArchiveTo(f) 416 if err != nil { 417 return nil, fmt.Errorf("cannot bundle charm: %v", err) 418 } 419 if _, err := f.Seek(0, 0); err != nil { 420 return nil, err 421 } 422 case *charm.CharmArchive: 423 var err error 424 if f, err = os.Open(ch.Path); err != nil { 425 return nil, fmt.Errorf("cannot read charm bundle: %v", err) 426 } 427 defer f.Close() 428 default: 429 return nil, fmt.Errorf("unknown charm type %T", ch) 430 } 431 digest, size, err := utils.ReadSHA256(f) 432 if err != nil { 433 return nil, err 434 } 435 if _, err := f.Seek(0, 0); err != nil { 436 return nil, err 437 } 438 439 stor := statestorage.NewStorage(st.EnvironUUID(), st.MongoSession()) 440 storagePath := fmt.Sprintf("/charms/%s-%s", curl.String(), digest) 441 if err := stor.Put(storagePath, f, size); err != nil { 442 return nil, fmt.Errorf("cannot put charm: %v", err) 443 } 444 sch, err := st.AddCharm(ch, curl, storagePath, digest) 445 if err != nil { 446 return nil, fmt.Errorf("cannot add charm: %v", err) 447 } 448 return sch, nil 449 } 450 451 func (s *JujuConnSuite) writeSampleConfig(c *gc.C, path string) { 452 if s.DummyConfig == nil { 453 s.DummyConfig = dummy.SampleConfig() 454 } 455 attrs := s.DummyConfig.Merge(testing.Attrs{ 456 "admin-secret": AdminSecret, 457 "agent-version": version.Current.Number.String(), 458 }).Delete("name") 459 whole := map[string]interface{}{ 460 "environments": map[string]interface{}{ 461 "dummyenv": attrs, 462 }, 463 } 464 data, err := goyaml.Marshal(whole) 465 c.Assert(err, jc.ErrorIsNil) 466 s.WriteConfig(string(data)) 467 } 468 469 type GetStater interface { 470 GetStateInAPIServer() *state.State 471 } 472 473 func (s *JujuConnSuite) tearDownConn(c *gc.C) { 474 testServer := gitjujutesting.MgoServer.Addr() 475 serverAlive := testServer != "" 476 477 // Close any api connections we know about first. 478 for _, st := range s.apiStates { 479 err := st.Close() 480 if serverAlive { 481 c.Check(err, jc.ErrorIsNil) 482 } 483 } 484 s.apiStates = nil 485 if s.APIState != nil { 486 err := s.APIState.Close() 487 s.APIState = nil 488 if serverAlive { 489 c.Check(err, gc.IsNil, 490 gc.Commentf("closing api state failed\n%s\n", errors.ErrorStack(err)), 491 ) 492 } 493 } 494 // Close state. 495 if s.State != nil { 496 err := s.State.Close() 497 if serverAlive { 498 // This happens way too often with failing tests, 499 // so add some context in case of an error. 500 c.Check(err, gc.IsNil, 501 gc.Commentf("closing state failed\n%s\n", errors.ErrorStack(err)), 502 ) 503 } 504 s.State = nil 505 } 506 507 dummy.Reset() 508 utils.SetHome(s.oldHome) 509 osenv.SetJujuHome(s.oldJujuHome) 510 s.oldHome = "" 511 s.RootDir = "" 512 } 513 514 func (s *JujuConnSuite) DataDir() string { 515 if s.RootDir == "" { 516 panic("DataDir called out of test context") 517 } 518 return filepath.Join(s.RootDir, "/var/lib/juju") 519 } 520 521 // WriteConfig writes a juju config file to the "home" directory. 522 func (s *JujuConnSuite) WriteConfig(configData string) { 523 if s.RootDir == "" { 524 panic("SetUpTest has not been called; will not overwrite $JUJU_HOME/environments.yaml") 525 } 526 path := osenv.JujuHomePath("environments.yaml") 527 err := ioutil.WriteFile(path, []byte(configData), 0600) 528 if err != nil { 529 panic(err) 530 } 531 } 532 533 func (s *JujuConnSuite) AddTestingCharm(c *gc.C, name string) *state.Charm { 534 ch := testcharms.Repo.CharmDir(name) 535 ident := fmt.Sprintf("%s-%d", ch.Meta().Name, ch.Revision()) 536 curl := charm.MustParseURL("local:quantal/" + ident) 537 repo, err := charm.InferRepository(curl.Reference(), testcharms.Repo.Path()) 538 c.Assert(err, jc.ErrorIsNil) 539 sch, err := PutCharm(s.State, curl, repo, false) 540 c.Assert(err, jc.ErrorIsNil) 541 return sch 542 } 543 544 func (s *JujuConnSuite) AddTestingService(c *gc.C, name string, ch *state.Charm) *state.Service { 545 return s.AddTestingServiceWithNetworks(c, name, ch, nil) 546 } 547 548 func (s *JujuConnSuite) AddTestingServiceWithStorage(c *gc.C, name string, ch *state.Charm, storage map[string]state.StorageConstraints) *state.Service { 549 owner := s.AdminUserTag(c).String() 550 service, err := s.State.AddService(name, owner, ch, nil, storage) 551 c.Assert(err, jc.ErrorIsNil) 552 return service 553 } 554 555 func (s *JujuConnSuite) AddTestingServiceWithNetworks(c *gc.C, name string, ch *state.Charm, networks []string) *state.Service { 556 c.Assert(s.State, gc.NotNil) 557 owner := s.AdminUserTag(c).String() 558 service, err := s.State.AddService(name, owner, ch, networks, nil) 559 c.Assert(err, jc.ErrorIsNil) 560 return service 561 } 562 563 func (s *JujuConnSuite) AgentConfigForTag(c *gc.C, tag names.Tag) agent.ConfigSetter { 564 password, err := utils.RandomPassword() 565 c.Assert(err, jc.ErrorIsNil) 566 config, err := agent.NewAgentConfig( 567 agent.AgentConfigParams{ 568 DataDir: s.DataDir(), 569 Tag: tag, 570 UpgradedToVersion: version.Current.Number, 571 Password: password, 572 Nonce: "nonce", 573 StateAddresses: s.MongoInfo(c).Addrs, 574 APIAddresses: s.APIInfo(c).Addrs, 575 CACert: testing.CACert, 576 Environment: s.State.EnvironTag(), 577 }) 578 c.Assert(err, jc.ErrorIsNil) 579 return config 580 } 581 582 // AssertConfigParameterUpdated updates environment parameter and 583 // asserts that no errors were encountered 584 func (s *JujuConnSuite) AssertConfigParameterUpdated(c *gc.C, key string, value interface{}) { 585 err := s.BackingState.UpdateEnvironConfig(map[string]interface{}{key: value}, nil, nil) 586 c.Assert(err, jc.ErrorIsNil) 587 }