github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/mongo/mongo_test.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package mongo_test 5 6 import ( 7 "encoding/base64" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 stdtesting "testing" 17 18 "github.com/juju/errors" 19 "github.com/juju/loggo" 20 "github.com/juju/testing" 21 jc "github.com/juju/testing/checkers" 22 "github.com/juju/utils" 23 "github.com/juju/utils/packaging/manager" 24 gc "gopkg.in/check.v1" 25 26 "github.com/juju/juju/mongo" 27 "github.com/juju/juju/network" 28 "github.com/juju/juju/service/common" 29 svctesting "github.com/juju/juju/service/common/testing" 30 coretesting "github.com/juju/juju/testing" 31 "github.com/juju/juju/version" 32 ) 33 34 func Test(t *stdtesting.T) { 35 //TODO(bogdanteleaga): Fix these on windows 36 if runtime.GOOS == "windows" { 37 t.Skip("bug 1403084: Skipping for now on windows") 38 } 39 gc.TestingT(t) 40 } 41 42 type MongoSuite struct { 43 coretesting.BaseSuite 44 mongodConfigPath string 45 mongodPath string 46 47 data *svctesting.FakeServiceData 48 } 49 50 var _ = gc.Suite(&MongoSuite{}) 51 52 var testInfo = struct { 53 StatePort int 54 Cert string 55 PrivateKey string 56 SharedSecret string 57 }{ 58 StatePort: 25252, 59 Cert: "foobar-cert", 60 PrivateKey: "foobar-privkey", 61 SharedSecret: "foobar-sharedsecret", 62 } 63 64 func makeEnsureServerParams(dataDir, namespace string) mongo.EnsureServerParams { 65 return mongo.EnsureServerParams{ 66 StatePort: testInfo.StatePort, 67 Cert: testInfo.Cert, 68 PrivateKey: testInfo.PrivateKey, 69 SharedSecret: testInfo.SharedSecret, 70 71 DataDir: dataDir, 72 Namespace: namespace, 73 } 74 } 75 76 func (s *MongoSuite) SetUpTest(c *gc.C) { 77 s.BaseSuite.SetUpTest(c) 78 // Try to make sure we don't execute any commands accidentally. 79 s.PatchEnvironment("PATH", "") 80 81 s.mongodPath = filepath.Join(c.MkDir(), "mongod") 82 err := ioutil.WriteFile(s.mongodPath, []byte("#!/bin/bash\n\nprintf %s 'db version v2.4.9'\n"), 0755) 83 c.Assert(err, jc.ErrorIsNil) 84 s.PatchValue(&mongo.JujuMongodPath, s.mongodPath) 85 86 // Patch "df" such that it always reports there's 1MB free. 87 s.PatchValue(mongo.AvailSpace, func(dir string) (float64, error) { 88 info, err := os.Stat(dir) 89 if err != nil { 90 return 0, err 91 } 92 if info.IsDir() { 93 return 1, nil 94 95 } 96 return 0, fmt.Errorf("not a directory") 97 }) 98 s.PatchValue(mongo.MinOplogSizeMB, 1) 99 100 testPath := c.MkDir() 101 s.mongodConfigPath = filepath.Join(testPath, "mongodConfig") 102 s.PatchValue(mongo.MongoConfigPath, s.mongodConfigPath) 103 104 s.data = svctesting.NewFakeServiceData() 105 mongo.PatchService(s.PatchValue, s.data) 106 } 107 108 func (s *MongoSuite) TestJujuMongodPath(c *gc.C) { 109 obtained, err := mongo.Path() 110 c.Check(err, jc.ErrorIsNil) 111 c.Check(obtained, gc.Equals, s.mongodPath) 112 } 113 114 func (s *MongoSuite) TestDefaultMongodPath(c *gc.C) { 115 s.PatchValue(&mongo.JujuMongodPath, "/not/going/to/exist/mongod") 116 s.PatchEnvPathPrepend(filepath.Dir(s.mongodPath)) 117 118 obtained, err := mongo.Path() 119 c.Check(err, jc.ErrorIsNil) 120 c.Check(obtained, gc.Equals, s.mongodPath) 121 } 122 123 func (s *MongoSuite) TestMakeJournalDirs(c *gc.C) { 124 dir := c.MkDir() 125 err := mongo.MakeJournalDirs(dir) 126 c.Assert(err, jc.ErrorIsNil) 127 128 testJournalDirs(dir, c) 129 } 130 131 func testJournalDirs(dir string, c *gc.C) { 132 journalDir := path.Join(dir, "journal") 133 134 c.Assert(journalDir, jc.IsDirectory) 135 info, err := os.Stat(filepath.Join(journalDir, "prealloc.0")) 136 c.Assert(err, jc.ErrorIsNil) 137 138 size := int64(1024 * 1024) 139 140 c.Assert(info.Size(), gc.Equals, size) 141 info, err = os.Stat(filepath.Join(journalDir, "prealloc.1")) 142 c.Assert(err, jc.ErrorIsNil) 143 c.Assert(info.Size(), gc.Equals, size) 144 info, err = os.Stat(filepath.Join(journalDir, "prealloc.2")) 145 c.Assert(err, jc.ErrorIsNil) 146 c.Assert(info.Size(), gc.Equals, size) 147 } 148 149 func (s *MongoSuite) TestEnsureServer(c *gc.C) { 150 dataDir := s.testEnsureServerNumaCtl(c, false) 151 152 contents, err := ioutil.ReadFile(s.mongodConfigPath) 153 c.Assert(err, jc.ErrorIsNil) 154 c.Assert(contents, jc.DeepEquals, []byte("ENABLE_MONGODB=no")) 155 156 contents, err = ioutil.ReadFile(mongo.SSLKeyPath(dataDir)) 157 c.Assert(err, jc.ErrorIsNil) 158 c.Assert(string(contents), gc.Equals, testInfo.Cert+"\n"+testInfo.PrivateKey) 159 160 contents, err = ioutil.ReadFile(mongo.SharedSecretPath(dataDir)) 161 c.Assert(err, jc.ErrorIsNil) 162 c.Assert(string(contents), gc.Equals, testInfo.SharedSecret) 163 164 // make sure that we log the version of mongodb as we get ready to 165 // start it 166 tlog := c.GetTestLog() 167 any := `(.|\n)*` 168 start := "^" + any 169 tail := any + "$" 170 c.Assert(tlog, gc.Matches, start+`using mongod: .*/mongod --version: "db version v2\.4\.9`+tail) 171 } 172 173 func (s *MongoSuite) TestEnsureServerServerExistsAndRunning(c *gc.C) { 174 dataDir := c.MkDir() 175 namespace := "namespace" 176 177 mockShellCommand(c, &s.CleanupSuite, "apt-get") 178 179 s.data.SetStatus(mongo.ServiceName(namespace), "running") 180 s.data.SetErrors(nil, nil, nil, errors.New("shouldn't be called")) 181 182 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace)) 183 c.Assert(err, jc.ErrorIsNil) 184 185 c.Check(s.data.Installed(), gc.HasLen, 0) 186 s.data.CheckCallNames(c, "Installed", "Exists", "Running") 187 } 188 189 func (s *MongoSuite) TestEnsureServerServerExistsNotRunningIsStarted(c *gc.C) { 190 dataDir := c.MkDir() 191 namespace := "namespace" 192 193 mockShellCommand(c, &s.CleanupSuite, "apt-get") 194 195 s.data.SetStatus(mongo.ServiceName(namespace), "installed") 196 197 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace)) 198 c.Assert(err, jc.ErrorIsNil) 199 200 c.Check(s.data.Installed(), gc.HasLen, 0) 201 s.data.CheckCallNames(c, "Installed", "Exists", "Running", "Start") 202 } 203 204 func (s *MongoSuite) TestEnsureServerServerExistsNotRunningStartError(c *gc.C) { 205 dataDir := c.MkDir() 206 namespace := "namespace" 207 208 mockShellCommand(c, &s.CleanupSuite, "apt-get") 209 210 s.data.SetStatus(mongo.ServiceName(namespace), "installed") 211 failure := errors.New("won't start") 212 s.data.SetErrors(nil, nil, nil, failure) // Installed, Exists, Running, Running, Start 213 214 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace)) 215 216 c.Check(errors.Cause(err), gc.Equals, failure) 217 c.Check(s.data.Installed(), gc.HasLen, 0) 218 s.data.CheckCallNames(c, "Installed", "Exists", "Running", "Start") 219 } 220 221 func (s *MongoSuite) TestEnsureServerNumaCtl(c *gc.C) { 222 s.testEnsureServerNumaCtl(c, true) 223 } 224 225 func (s *MongoSuite) testEnsureServerNumaCtl(c *gc.C, setNumaPolicy bool) string { 226 dataDir := c.MkDir() 227 dbDir := filepath.Join(dataDir, "db") 228 namespace := "namespace" 229 230 mockShellCommand(c, &s.CleanupSuite, "apt-get") 231 232 testParams := makeEnsureServerParams(dataDir, namespace) 233 testParams.SetNumaControlPolicy = setNumaPolicy 234 err := mongo.EnsureServer(testParams) 235 c.Assert(err, jc.ErrorIsNil) 236 237 testJournalDirs(dbDir, c) 238 239 assertInstalled := func() { 240 installed := s.data.Installed() 241 c.Assert(installed, gc.HasLen, 1) 242 service := installed[0] 243 c.Assert(service.Name(), gc.Equals, "juju-db-namespace") 244 c.Assert(service.Conf().Desc, gc.Equals, "juju state database") 245 if setNumaPolicy { 246 stripped := strings.Replace(service.Conf().ExtraScript, "\n", "", -1) 247 c.Assert(stripped, gc.Matches, `.* sysctl .*`) 248 } else { 249 c.Assert(service.Conf().ExtraScript, gc.Equals, "") 250 } 251 c.Assert(service.Conf().ExecStart, gc.Matches, ".*"+regexp.QuoteMeta(s.mongodPath)+".*") 252 c.Assert(service.Conf().Logfile, gc.Equals, "") 253 } 254 assertInstalled() 255 return dataDir 256 } 257 258 func (s *MongoSuite) TestInstallMongod(c *gc.C) { 259 type installs struct { 260 series string 261 pkg string 262 } 263 tests := []installs{ 264 {"precise", "mongodb-server"}, 265 {"quantal", "mongodb-server"}, 266 {"raring", "mongodb-server"}, 267 {"saucy", "mongodb-server"}, 268 {"trusty", "juju-mongodb"}, 269 {"u-series", "juju-mongodb"}, 270 } 271 272 mockShellCommand(c, &s.CleanupSuite, "add-apt-repository") 273 output := mockShellCommand(c, &s.CleanupSuite, "apt-get") 274 for _, test := range tests { 275 c.Logf("Testing %s", test.series) 276 dataDir := c.MkDir() 277 namespace := "namespace" + test.series 278 279 s.PatchValue(&version.Current.Series, test.series) 280 281 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace)) 282 c.Assert(err, jc.ErrorIsNil) 283 284 cmds := getMockShellCalls(c, output) 285 286 // quantal does an extra apt-get install for python software properties 287 // so we need to remember to skip that one 288 index := 0 289 if test.series == "quantal" { 290 index = 1 291 } 292 match := fmt.Sprintf(`.* install .*%s`, test.pkg) 293 c.Assert(strings.Join(cmds[index], " "), gc.Matches, match) 294 // remove the temp file between tests 295 c.Assert(os.Remove(output), gc.IsNil) 296 } 297 } 298 299 func (s *MongoSuite) TestMongoAptGetFails(c *gc.C) { 300 s.PatchValue(&version.Current.Series, "trusty") 301 302 // Any exit code from apt-get that isn't 0 or 100 will be treated 303 // as unexpected, skipping the normal retry loop. failCmd causes 304 // the command to exit with 1. 305 binDir := c.MkDir() 306 s.PatchEnvPathPrepend(binDir) 307 failCmd(filepath.Join(binDir, "apt-get")) 308 309 // Set the mongodb service as installed but not running. 310 namespace := "namespace" 311 s.data.SetStatus(mongo.ServiceName(namespace), "installed") 312 313 var tw loggo.TestWriter 314 c.Assert(loggo.RegisterWriter("test-writer", &tw, loggo.ERROR), jc.ErrorIsNil) 315 defer loggo.RemoveWriter("test-writer") 316 317 dataDir := c.MkDir() 318 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace)) 319 320 // Even though apt-get failed, EnsureServer should continue and 321 // not return the error - even though apt-get failed, the Juju 322 // mongodb package is most likely already installed. 323 // The error should be logged however. 324 c.Assert(err, jc.ErrorIsNil) 325 326 c.Check(tw.Log(), jc.LogMatches, []jc.SimpleMessage{ 327 {loggo.ERROR, `packaging command failed: .+`}, 328 {loggo.ERROR, `cannot install/upgrade mongod \(will proceed anyway\): packaging command failed`}, 329 }) 330 331 // Verify that EnsureServer continued and started the mongodb service. 332 c.Check(s.data.Installed(), gc.HasLen, 0) 333 s.data.CheckCallNames(c, "Installed", "Exists", "Running", "Start") 334 } 335 336 func (s *MongoSuite) TestInstallMongodServiceExists(c *gc.C) { 337 output := mockShellCommand(c, &s.CleanupSuite, "apt-get") 338 dataDir := c.MkDir() 339 namespace := "namespace" 340 341 s.data.SetStatus(mongo.ServiceName(namespace), "running") 342 s.data.SetErrors(nil, nil, nil, errors.New("shouldn't be called")) 343 344 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace)) 345 c.Assert(err, jc.ErrorIsNil) 346 347 c.Check(s.data.Installed(), gc.HasLen, 0) 348 s.data.CheckCallNames(c, "Installed", "Exists", "Running") 349 350 // We still attempt to install mongodb, despite the service existing. 351 cmds := getMockShellCalls(c, output) 352 c.Check(cmds, gc.HasLen, 1) 353 } 354 355 func (s *MongoSuite) TestNewServiceWithReplSet(c *gc.C) { 356 dataDir := c.MkDir() 357 358 conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, false) 359 c.Assert(strings.Contains(conf.ExecStart, "--replSet"), jc.IsTrue) 360 } 361 362 func (s *MongoSuite) TestNewServiceWithNumCtl(c *gc.C) { 363 dataDir := c.MkDir() 364 365 conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, true) 366 c.Assert(conf.ExtraScript, gc.Not(gc.Matches), "") 367 } 368 369 func (s *MongoSuite) TestNewServiceIPv6(c *gc.C) { 370 dataDir := c.MkDir() 371 372 conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, false) 373 c.Assert(strings.Contains(conf.ExecStart, "--ipv6"), jc.IsTrue) 374 } 375 376 func (s *MongoSuite) TestNewServiceWithJournal(c *gc.C) { 377 dataDir := c.MkDir() 378 379 conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, false) 380 c.Assert(conf.ExecStart, gc.Matches, `.* --journal.*`) 381 } 382 383 func (s *MongoSuite) TestNoAuthCommandWithJournal(c *gc.C) { 384 dataDir := c.MkDir() 385 386 cmd, err := mongo.NoauthCommand(dataDir, 1234) 387 c.Assert(err, jc.ErrorIsNil) 388 var isJournalPresent bool 389 for _, value := range cmd.Args { 390 if value == "--journal" { 391 isJournalPresent = true 392 } 393 } 394 c.Assert(isJournalPresent, jc.IsTrue) 395 } 396 397 func (s *MongoSuite) TestRemoveService(c *gc.C) { 398 namespace := "namespace" 399 s.data.SetStatus(mongo.ServiceName(namespace), "running") 400 401 err := mongo.RemoveService(namespace) 402 c.Assert(err, jc.ErrorIsNil) 403 404 removed := s.data.Removed() 405 if !c.Check(removed, gc.HasLen, 1) { 406 c.Check(removed[0].Name(), gc.Equals, "juju-db-namespace") 407 c.Check(removed[0].Conf(), jc.DeepEquals, common.Conf{}) 408 } 409 s.data.CheckCallNames(c, "Stop", "Remove") 410 } 411 412 func (s *MongoSuite) TestQuantalAptAddRepo(c *gc.C) { 413 dir := c.MkDir() 414 // patch manager.RunCommandWithRetry for repository addition: 415 s.PatchValue(&manager.RunCommandWithRetry, func(string) (string, int, error) { 416 return "", 1, fmt.Errorf("packaging command failed: exit status 1") 417 }) 418 s.PatchEnvPathPrepend(dir) 419 failCmd(filepath.Join(dir, "add-apt-repository")) 420 mockShellCommand(c, &s.CleanupSuite, "apt-get") 421 422 var tw loggo.TestWriter 423 c.Assert(loggo.RegisterWriter("test-writer", &tw, loggo.ERROR), jc.ErrorIsNil) 424 defer loggo.RemoveWriter("test-writer") 425 426 // test that we call add-apt-repository only for quantal 427 // (and that if it fails, we log the error) 428 s.PatchValue(&version.Current.Series, "quantal") 429 err := mongo.EnsureServer(makeEnsureServerParams(dir, "")) 430 c.Assert(err, jc.ErrorIsNil) 431 432 c.Assert(tw.Log(), jc.LogMatches, []jc.SimpleMessage{ 433 {loggo.ERROR, `cannot install/upgrade mongod \(will proceed anyway\): packaging command failed`}, 434 }) 435 436 s.PatchValue(&manager.RunCommandWithRetry, func(string) (string, int, error) { 437 return "", 0, nil 438 }) 439 s.PatchValue(&version.Current.Series, "trusty") 440 failCmd(filepath.Join(dir, "mongod")) 441 err = mongo.EnsureServer(makeEnsureServerParams(dir, "")) 442 c.Assert(err, jc.ErrorIsNil) 443 } 444 445 func (s *MongoSuite) TestNoMongoDir(c *gc.C) { 446 // Make a non-existent directory that can nonetheless be 447 // created. 448 mockShellCommand(c, &s.CleanupSuite, "apt-get") 449 dataDir := filepath.Join(c.MkDir(), "dir", "data") 450 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, "")) 451 c.Check(err, jc.ErrorIsNil) 452 453 _, err = os.Stat(filepath.Join(dataDir, "db")) 454 c.Assert(err, jc.ErrorIsNil) 455 } 456 457 func (s *MongoSuite) TestServiceName(c *gc.C) { 458 name := mongo.ServiceName("foo") 459 c.Assert(name, gc.Equals, "juju-db-foo") 460 name = mongo.ServiceName("") 461 c.Assert(name, gc.Equals, "juju-db") 462 } 463 464 func (s *MongoSuite) TestSelectPeerAddress(c *gc.C) { 465 addresses := []network.Address{{ 466 Value: "10.0.0.1", 467 Type: network.IPv4Address, 468 NetworkName: "cloud", 469 Scope: network.ScopeCloudLocal}, { 470 Value: "8.8.8.8", 471 Type: network.IPv4Address, 472 NetworkName: "public", 473 Scope: network.ScopePublic}} 474 475 address := mongo.SelectPeerAddress(addresses) 476 c.Assert(address, gc.Equals, "10.0.0.1") 477 } 478 479 func (s *MongoSuite) TestSelectPeerHostPort(c *gc.C) { 480 481 hostPorts := []network.HostPort{{ 482 Address: network.Address{ 483 Value: "10.0.0.1", 484 Type: network.IPv4Address, 485 NetworkName: "cloud", 486 Scope: network.ScopeCloudLocal, 487 }, 488 Port: 37017}, { 489 Address: network.Address{ 490 Value: "8.8.8.8", 491 Type: network.IPv4Address, 492 NetworkName: "public", 493 Scope: network.ScopePublic, 494 }, 495 Port: 37017}} 496 497 address := mongo.SelectPeerHostPort(hostPorts) 498 c.Assert(address, gc.Equals, "10.0.0.1:37017") 499 } 500 501 func (s *MongoSuite) TestGenerateSharedSecret(c *gc.C) { 502 secret, err := mongo.GenerateSharedSecret() 503 c.Assert(err, jc.ErrorIsNil) 504 c.Assert(secret, gc.HasLen, 1024) 505 _, err = base64.StdEncoding.DecodeString(secret) 506 c.Assert(err, jc.ErrorIsNil) 507 } 508 509 func (s *MongoSuite) TestAddPPAInQuantal(c *gc.C) { 510 mockShellCommand(c, &s.CleanupSuite, "apt-get") 511 512 addAptRepoOut := mockShellCommand(c, &s.CleanupSuite, "add-apt-repository") 513 s.PatchValue(&version.Current.Series, "quantal") 514 515 dataDir := c.MkDir() 516 err := mongo.EnsureServer(makeEnsureServerParams(dataDir, "")) 517 c.Assert(err, jc.ErrorIsNil) 518 519 c.Assert(getMockShellCalls(c, addAptRepoOut), gc.DeepEquals, [][]string{{ 520 "--yes", 521 "\"ppa:juju/stable\"", 522 }}) 523 } 524 525 // mockShellCommand creates a new command with the given 526 // name and contents, and patches $PATH so that it will be 527 // executed by preference. It returns the name of a file 528 // that is written by each call to the command - mockShellCalls 529 // can be used to retrieve the calls. 530 func mockShellCommand(c *gc.C, s *testing.CleanupSuite, name string) string { 531 dir := c.MkDir() 532 s.PatchEnvPathPrepend(dir) 533 534 // Note the shell script produces output of the form: 535 // +arg1+\n 536 // +arg2+\n 537 // ... 538 // +argn+\n 539 // - 540 // 541 // It would be nice if there was a simple way of unambiguously 542 // quoting shell arguments, but this will do as long 543 // as no argument contains a newline character. 544 outputFile := filepath.Join(dir, name+".out") 545 contents := `#!/bin/sh 546 { 547 for i in "$@"; do 548 echo +"$i"+ 549 done 550 echo - 551 } >> ` + utils.ShQuote(outputFile) + ` 552 ` 553 err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0755) 554 c.Assert(err, jc.ErrorIsNil) 555 return outputFile 556 } 557 558 // getMockShellCalls, given a file name returned by mockShellCommands, 559 // returns a slice containing one element for each call, each 560 // containing the arguments passed to the command. 561 // It will be confused if the arguments contain newlines. 562 func getMockShellCalls(c *gc.C, file string) [][]string { 563 data, err := ioutil.ReadFile(file) 564 if os.IsNotExist(err) { 565 return nil 566 } 567 c.Assert(err, jc.ErrorIsNil) 568 s := string(data) 569 parts := strings.Split(s, "\n-\n") 570 c.Assert(parts[len(parts)-1], gc.Equals, "") 571 var calls [][]string 572 for _, part := range parts[0 : len(parts)-1] { 573 calls = append(calls, splitCall(c, part)) 574 } 575 return calls 576 } 577 578 // splitCall splits the output produced by a single call to the 579 // mocked shell function (see mockShellCommand) and 580 // splits it into its individual arguments. 581 func splitCall(c *gc.C, part string) []string { 582 var result []string 583 for _, arg := range strings.Split(part, "\n") { 584 c.Assert(arg, gc.Matches, `\+.*\+`) 585 arg = strings.TrimSuffix(arg, "+") 586 arg = strings.TrimPrefix(arg, "+") 587 result = append(result, arg) 588 } 589 return result 590 } 591 592 // failCmd creates an executable file at the given location that will do nothing 593 // except return an error. 594 func failCmd(path string) { 595 err := ioutil.WriteFile(path, []byte("#!/bin/bash --norc\nexit 1"), 0755) 596 if err != nil { 597 panic(err) 598 } 599 }