github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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 "strings" 15 stdtesting "testing" 16 17 "github.com/juju/testing" 18 jc "github.com/juju/testing/checkers" 19 "github.com/juju/utils" 20 gc "launchpad.net/gocheck" 21 22 "github.com/juju/juju/mongo" 23 "github.com/juju/juju/network" 24 "github.com/juju/juju/state/api/params" 25 coretesting "github.com/juju/juju/testing" 26 "github.com/juju/juju/upstart" 27 "github.com/juju/juju/version" 28 ) 29 30 func Test(t *stdtesting.T) { gc.TestingT(t) } 31 32 type MongoSuite struct { 33 coretesting.BaseSuite 34 mongodConfigPath string 35 mongodPath string 36 37 installError error 38 installed []upstart.Conf 39 40 removeError error 41 removed []upstart.Service 42 } 43 44 var _ = gc.Suite(&MongoSuite{}) 45 46 var testInfo = params.StateServingInfo{ 47 StatePort: 25252, 48 Cert: "foobar-cert", 49 PrivateKey: "foobar-privkey", 50 SharedSecret: "foobar-sharedsecret", 51 } 52 53 func (s *MongoSuite) SetUpTest(c *gc.C) { 54 s.BaseSuite.SetUpTest(c) 55 // Try to make sure we don't execute any commands accidentally. 56 s.PatchEnvironment("PATH", "") 57 58 s.mongodPath = filepath.Join(c.MkDir(), "mongod") 59 err := ioutil.WriteFile(s.mongodPath, []byte("#!/bin/bash\n\nprintf %s 'db version v2.4.9'\n"), 0755) 60 c.Assert(err, gc.IsNil) 61 s.PatchValue(&mongo.JujuMongodPath, s.mongodPath) 62 63 // Patch "df" such that it always reports there's 1MB free. 64 s.PatchValue(mongo.AvailSpace, func(dir string) (float64, error) { return 1, nil }) 65 s.PatchValue(mongo.MinOplogSizeMB, 1) 66 67 testPath := c.MkDir() 68 s.mongodConfigPath = filepath.Join(testPath, "mongodConfig") 69 s.PatchValue(mongo.MongoConfigPath, s.mongodConfigPath) 70 71 s.PatchValue(mongo.UpstartConfInstall, func(conf *upstart.Conf) error { 72 s.installed = append(s.installed, *conf) 73 return s.installError 74 }) 75 s.PatchValue(mongo.UpstartServiceStopAndRemove, func(svc *upstart.Service) error { 76 s.removed = append(s.removed, *svc) 77 return s.removeError 78 }) 79 // Clear out the values that are set by the above patched functions. 80 s.removeError = nil 81 s.installError = nil 82 s.installed = nil 83 s.removed = nil 84 } 85 86 func (s *MongoSuite) TestJujuMongodPath(c *gc.C) { 87 obtained, err := mongo.Path() 88 c.Check(err, gc.IsNil) 89 c.Check(obtained, gc.Equals, s.mongodPath) 90 } 91 92 func (s *MongoSuite) TestDefaultMongodPath(c *gc.C) { 93 s.PatchValue(&mongo.JujuMongodPath, "/not/going/to/exist/mongod") 94 s.PatchEnvPathPrepend(filepath.Dir(s.mongodPath)) 95 96 obtained, err := mongo.Path() 97 c.Check(err, gc.IsNil) 98 c.Check(obtained, gc.Equals, s.mongodPath) 99 } 100 101 func (s *MongoSuite) TestMakeJournalDirs(c *gc.C) { 102 dir := c.MkDir() 103 err := mongo.MakeJournalDirs(dir) 104 c.Assert(err, gc.IsNil) 105 106 testJournalDirs(dir, c) 107 } 108 109 func testJournalDirs(dir string, c *gc.C) { 110 journalDir := path.Join(dir, "journal") 111 112 c.Assert(journalDir, jc.IsDirectory) 113 info, err := os.Stat(filepath.Join(journalDir, "prealloc.0")) 114 c.Assert(err, gc.IsNil) 115 116 size := int64(1024 * 1024) 117 118 c.Assert(info.Size(), gc.Equals, size) 119 info, err = os.Stat(filepath.Join(journalDir, "prealloc.1")) 120 c.Assert(err, gc.IsNil) 121 c.Assert(info.Size(), gc.Equals, size) 122 info, err = os.Stat(filepath.Join(journalDir, "prealloc.2")) 123 c.Assert(err, gc.IsNil) 124 c.Assert(info.Size(), gc.Equals, size) 125 } 126 127 func (s *MongoSuite) TestEnsureServer(c *gc.C) { 128 dataDir := c.MkDir() 129 dbDir := filepath.Join(dataDir, "db") 130 namespace := "namespace" 131 132 mockShellCommand(c, &s.CleanupSuite, "apt-get") 133 134 err := mongo.EnsureServer(dataDir, namespace, testInfo) 135 c.Assert(err, gc.IsNil) 136 137 testJournalDirs(dbDir, c) 138 139 assertInstalled := func() { 140 c.Assert(s.installed, gc.HasLen, 1) 141 conf := s.installed[0] 142 c.Assert(conf.Name, gc.Equals, "juju-db-namespace") 143 c.Assert(conf.InitDir, gc.Equals, "/etc/init") 144 c.Assert(conf.Desc, gc.Equals, "juju state database") 145 c.Assert(conf.Cmd, gc.Matches, regexp.QuoteMeta(s.mongodPath)+".*") 146 // TODO(nate) set Out so that mongod output goes somewhere useful? 147 c.Assert(conf.Out, gc.Equals, "") 148 } 149 assertInstalled() 150 151 contents, err := ioutil.ReadFile(s.mongodConfigPath) 152 c.Assert(err, gc.IsNil) 153 c.Assert(contents, jc.DeepEquals, []byte("ENABLE_MONGODB=no")) 154 155 contents, err = ioutil.ReadFile(mongo.SSLKeyPath(dataDir)) 156 c.Assert(err, gc.IsNil) 157 c.Assert(string(contents), gc.Equals, testInfo.Cert+"\n"+testInfo.PrivateKey) 158 159 contents, err = ioutil.ReadFile(mongo.SharedSecretPath(dataDir)) 160 c.Assert(err, gc.IsNil) 161 c.Assert(string(contents), gc.Equals, testInfo.SharedSecret) 162 163 s.installed = nil 164 // now check we can call it multiple times without error 165 err = mongo.EnsureServer(dataDir, namespace, testInfo) 166 c.Assert(err, gc.IsNil) 167 assertInstalled() 168 169 // make sure that we log the version of mongodb as we get ready to 170 // start it 171 tlog := c.GetTestLog() 172 any := `(.|\n)*` 173 start := "^" + any 174 tail := any + "$" 175 c.Assert(tlog, gc.Matches, start+`using mongod: .*/mongod --version: "db version v2\.4\.9`+tail) 176 } 177 178 func (s *MongoSuite) TestInstallMongod(c *gc.C) { 179 type installs struct { 180 series string 181 pkg string 182 } 183 tests := []installs{ 184 {"precise", "mongodb-server"}, 185 {"quantal", "mongodb-server"}, 186 {"raring", "mongodb-server"}, 187 {"saucy", "mongodb-server"}, 188 {"trusty", "juju-mongodb"}, 189 {"u-series", "juju-mongodb"}, 190 } 191 192 mockShellCommand(c, &s.CleanupSuite, "add-apt-repository") 193 output := mockShellCommand(c, &s.CleanupSuite, "apt-get") 194 for _, test := range tests { 195 c.Logf("Testing %s", test.series) 196 dataDir := c.MkDir() 197 namespace := "namespace" + test.series 198 199 s.PatchValue(&version.Current.Series, test.series) 200 201 err := mongo.EnsureServer(dataDir, namespace, testInfo) 202 c.Assert(err, gc.IsNil) 203 204 cmds := getMockShellCalls(c, output) 205 206 // quantal does an extra apt-get install for python software properties 207 // so we need to remember to skip that one 208 index := 0 209 if test.series == "quantal" { 210 index = 1 211 } 212 match := fmt.Sprintf(`.* install .*%s`, test.pkg) 213 c.Assert(strings.Join(cmds[index], " "), gc.Matches, match) 214 // remove the temp file between tests 215 c.Assert(os.Remove(output), gc.IsNil) 216 } 217 } 218 219 func (s *MongoSuite) TestUpstartServiceWithReplSet(c *gc.C) { 220 dataDir := c.MkDir() 221 222 svc, _, err := mongo.UpstartService("", dataDir, dataDir, 1234) 223 c.Assert(err, gc.IsNil) 224 c.Assert(strings.Contains(svc.Cmd, "--replSet"), jc.IsTrue) 225 } 226 227 func (s *MongoSuite) TestUpstartServiceWithJournal(c *gc.C) { 228 dataDir := c.MkDir() 229 230 svc, _, err := mongo.UpstartService("", dataDir, dataDir, 1234) 231 c.Assert(err, gc.IsNil) 232 journalPresent := strings.Contains(svc.Cmd, " --journal ") || strings.HasSuffix(svc.Cmd, " --journal") 233 c.Assert(journalPresent, jc.IsTrue) 234 } 235 236 func (s *MongoSuite) TestNoAuthCommandWithJournal(c *gc.C) { 237 dataDir := c.MkDir() 238 239 cmd, err := mongo.NoauthCommand(dataDir, 1234) 240 c.Assert(err, gc.IsNil) 241 var isJournalPresent bool 242 for _, value := range cmd.Args { 243 if value == "--journal" { 244 isJournalPresent = true 245 } 246 } 247 c.Assert(isJournalPresent, jc.IsTrue) 248 } 249 250 func (s *MongoSuite) TestRemoveService(c *gc.C) { 251 err := mongo.RemoveService("namespace") 252 c.Assert(err, gc.IsNil) 253 c.Assert(s.removed, jc.DeepEquals, []upstart.Service{{ 254 Name: "juju-db-namespace", 255 InitDir: upstart.InitDir, 256 }}) 257 } 258 259 func (s *MongoSuite) TestQuantalAptAddRepo(c *gc.C) { 260 dir := c.MkDir() 261 s.PatchEnvPathPrepend(dir) 262 failCmd(filepath.Join(dir, "add-apt-repository")) 263 mockShellCommand(c, &s.CleanupSuite, "apt-get") 264 265 // test that we call add-apt-repository only for quantal (and that if it 266 // fails, we return the error) 267 s.PatchValue(&version.Current.Series, "quantal") 268 err := mongo.EnsureServer(dir, "", testInfo) 269 c.Assert(err, gc.ErrorMatches, "cannot install mongod: cannot add apt repository: exit status 1.*") 270 271 s.PatchValue(&version.Current.Series, "trusty") 272 err = mongo.EnsureServer(dir, "", testInfo) 273 c.Assert(err, gc.IsNil) 274 } 275 276 func (s *MongoSuite) TestNoMongoDir(c *gc.C) { 277 // Make a non-existent directory that can nonetheless be 278 // created. 279 mockShellCommand(c, &s.CleanupSuite, "apt-get") 280 dataDir := filepath.Join(c.MkDir(), "dir", "data") 281 err := mongo.EnsureServer(dataDir, "", testInfo) 282 c.Check(err, gc.IsNil) 283 284 _, err = os.Stat(filepath.Join(dataDir, "db")) 285 c.Assert(err, gc.IsNil) 286 } 287 288 func (s *MongoSuite) TestServiceName(c *gc.C) { 289 name := mongo.ServiceName("foo") 290 c.Assert(name, gc.Equals, "juju-db-foo") 291 name = mongo.ServiceName("") 292 c.Assert(name, gc.Equals, "juju-db") 293 } 294 295 func (s *MongoSuite) TestSelectPeerAddress(c *gc.C) { 296 addresses := []network.Address{{ 297 Value: "10.0.0.1", 298 Type: network.IPv4Address, 299 NetworkName: "cloud", 300 Scope: network.ScopeCloudLocal}, { 301 Value: "8.8.8.8", 302 Type: network.IPv4Address, 303 NetworkName: "public", 304 Scope: network.ScopePublic}} 305 306 address := mongo.SelectPeerAddress(addresses) 307 c.Assert(address, gc.Equals, "10.0.0.1") 308 } 309 310 func (s *MongoSuite) TestSelectPeerHostPort(c *gc.C) { 311 312 hostPorts := []network.HostPort{{ 313 Address: network.Address{ 314 Value: "10.0.0.1", 315 Type: network.IPv4Address, 316 NetworkName: "cloud", 317 Scope: network.ScopeCloudLocal, 318 }, 319 Port: 37017}, { 320 Address: network.Address{ 321 Value: "8.8.8.8", 322 Type: network.IPv4Address, 323 NetworkName: "public", 324 Scope: network.ScopePublic, 325 }, 326 Port: 37017}} 327 328 address := mongo.SelectPeerHostPort(hostPorts) 329 c.Assert(address, gc.Equals, "10.0.0.1:37017") 330 } 331 332 func (s *MongoSuite) TestGenerateSharedSecret(c *gc.C) { 333 secret, err := mongo.GenerateSharedSecret() 334 c.Assert(err, gc.IsNil) 335 c.Assert(secret, gc.HasLen, 1024) 336 _, err = base64.StdEncoding.DecodeString(secret) 337 c.Assert(err, gc.IsNil) 338 } 339 340 func (s *MongoSuite) TestAddPPAInQuantal(c *gc.C) { 341 mockShellCommand(c, &s.CleanupSuite, "apt-get") 342 343 addAptRepoOut := mockShellCommand(c, &s.CleanupSuite, "add-apt-repository") 344 s.PatchValue(&version.Current.Series, "quantal") 345 346 dataDir := c.MkDir() 347 err := mongo.EnsureServer(dataDir, "", testInfo) 348 c.Assert(err, gc.IsNil) 349 350 c.Assert(getMockShellCalls(c, addAptRepoOut), gc.DeepEquals, [][]string{{ 351 "-y", 352 "ppa:juju/stable", 353 }}) 354 } 355 356 // mockShellCommand creates a new command with the given 357 // name and contents, and patches $PATH so that it will be 358 // executed by preference. It returns the name of a file 359 // that is written by each call to the command - mockShellCalls 360 // can be used to retrieve the calls. 361 func mockShellCommand(c *gc.C, s *testing.CleanupSuite, name string) string { 362 dir := c.MkDir() 363 s.PatchEnvPathPrepend(dir) 364 365 // Note the shell script produces output of the form: 366 // +arg1+\n 367 // +arg2+\n 368 // ... 369 // +argn+\n 370 // - 371 // 372 // It would be nice if there was a simple way of unambiguously 373 // quoting shell arguments, but this will do as long 374 // as no argument contains a newline character. 375 outputFile := filepath.Join(dir, name+".out") 376 contents := `#!/bin/sh 377 { 378 for i in "$@"; do 379 echo +"$i"+ 380 done 381 echo - 382 } >> ` + utils.ShQuote(outputFile) + ` 383 ` 384 err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0755) 385 c.Assert(err, gc.IsNil) 386 return outputFile 387 } 388 389 // getMockShellCalls, given a file name returned by mockShellCommands, 390 // returns a slice containing one element for each call, each 391 // containing the arguments passed to the command. 392 // It will be confused if the arguments contain newlines. 393 func getMockShellCalls(c *gc.C, file string) [][]string { 394 data, err := ioutil.ReadFile(file) 395 if os.IsNotExist(err) { 396 return nil 397 } 398 c.Assert(err, gc.IsNil) 399 s := string(data) 400 parts := strings.Split(s, "\n-\n") 401 c.Assert(parts[len(parts)-1], gc.Equals, "") 402 var calls [][]string 403 for _, part := range parts[0 : len(parts)-1] { 404 calls = append(calls, splitCall(c, part)) 405 } 406 return calls 407 } 408 409 // splitCall splits the output produced by a single call to the 410 // mocked shell function (see mockShellCommand) and 411 // splits it into its individual arguments. 412 func splitCall(c *gc.C, part string) []string { 413 var result []string 414 for _, arg := range strings.Split(part, "\n") { 415 c.Assert(arg, gc.Matches, `\+.*\+`) 416 arg = strings.TrimSuffix(arg, "+") 417 arg = strings.TrimPrefix(arg, "+") 418 result = append(result, arg) 419 } 420 return result 421 } 422 423 // failCmd creates an executable file at the given location that will do nothing 424 // except return an error. 425 func failCmd(path string) { 426 err := ioutil.WriteFile(path, []byte("#!/bin/bash --norc\nexit 1"), 0755) 427 if err != nil { 428 panic(err) 429 } 430 }