github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/agent/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/agent/mongo" 23 "github.com/juju/juju/instance" 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 testPath := c.MkDir() 64 s.mongodConfigPath = filepath.Join(testPath, "mongodConfig") 65 s.PatchValue(mongo.MongoConfigPath, s.mongodConfigPath) 66 67 s.PatchValue(mongo.UpstartConfInstall, func(conf *upstart.Conf) error { 68 s.installed = append(s.installed, *conf) 69 return s.installError 70 }) 71 s.PatchValue(mongo.UpstartServiceStopAndRemove, func(svc *upstart.Service) error { 72 s.removed = append(s.removed, *svc) 73 return s.removeError 74 }) 75 // Clear out the values that are set by the above patched functions. 76 s.removeError = nil 77 s.installError = nil 78 s.installed = nil 79 s.removed = nil 80 } 81 82 func (s *MongoSuite) TestJujuMongodPath(c *gc.C) { 83 obtained, err := mongo.Path() 84 c.Check(err, gc.IsNil) 85 c.Check(obtained, gc.Equals, s.mongodPath) 86 } 87 88 func (s *MongoSuite) TestDefaultMongodPath(c *gc.C) { 89 s.PatchValue(&mongo.JujuMongodPath, "/not/going/to/exist/mongod") 90 s.PatchEnvPathPrepend(filepath.Dir(s.mongodPath)) 91 92 obtained, err := mongo.Path() 93 c.Check(err, gc.IsNil) 94 c.Check(obtained, gc.Equals, s.mongodPath) 95 } 96 97 func (s *MongoSuite) TestMakeJournalDirs(c *gc.C) { 98 dir := c.MkDir() 99 err := mongo.MakeJournalDirs(dir) 100 c.Assert(err, gc.IsNil) 101 102 testJournalDirs(dir, c) 103 } 104 105 func testJournalDirs(dir string, c *gc.C) { 106 journalDir := path.Join(dir, "journal") 107 108 c.Assert(journalDir, jc.IsDirectory) 109 info, err := os.Stat(filepath.Join(journalDir, "prealloc.0")) 110 c.Assert(err, gc.IsNil) 111 112 size := int64(1024 * 1024) 113 114 c.Assert(info.Size(), gc.Equals, size) 115 info, err = os.Stat(filepath.Join(journalDir, "prealloc.1")) 116 c.Assert(err, gc.IsNil) 117 c.Assert(info.Size(), gc.Equals, size) 118 info, err = os.Stat(filepath.Join(journalDir, "prealloc.2")) 119 c.Assert(err, gc.IsNil) 120 c.Assert(info.Size(), gc.Equals, size) 121 } 122 123 func (s *MongoSuite) TestEnsureServer(c *gc.C) { 124 dataDir := c.MkDir() 125 dbDir := filepath.Join(dataDir, "db") 126 namespace := "namespace" 127 128 mockShellCommand(c, &s.CleanupSuite, "apt-get") 129 130 err := mongo.EnsureServer(dataDir, namespace, testInfo, mongo.WithHA) 131 c.Assert(err, gc.IsNil) 132 133 testJournalDirs(dbDir, c) 134 135 assertInstalled := func() { 136 c.Assert(s.installed, gc.HasLen, 1) 137 conf := s.installed[0] 138 c.Assert(conf.Name, gc.Equals, "juju-db-namespace") 139 c.Assert(conf.InitDir, gc.Equals, "/etc/init") 140 c.Assert(conf.Desc, gc.Equals, "juju state database") 141 c.Assert(conf.Cmd, gc.Matches, regexp.QuoteMeta(s.mongodPath)+".*") 142 // TODO(nate) set Out so that mongod output goes somewhere useful? 143 c.Assert(conf.Out, gc.Equals, "") 144 } 145 assertInstalled() 146 147 contents, err := ioutil.ReadFile(s.mongodConfigPath) 148 c.Assert(err, gc.IsNil) 149 c.Assert(contents, jc.DeepEquals, []byte("ENABLE_MONGODB=no")) 150 151 contents, err = ioutil.ReadFile(mongo.SSLKeyPath(dataDir)) 152 c.Assert(err, gc.IsNil) 153 c.Assert(string(contents), gc.Equals, testInfo.Cert+"\n"+testInfo.PrivateKey) 154 155 contents, err = ioutil.ReadFile(mongo.SharedSecretPath(dataDir)) 156 c.Assert(err, gc.IsNil) 157 c.Assert(string(contents), gc.Equals, testInfo.SharedSecret) 158 159 s.installed = nil 160 // now check we can call it multiple times without error 161 err = mongo.EnsureServer(dataDir, namespace, testInfo, mongo.WithHA) 162 c.Assert(err, gc.IsNil) 163 assertInstalled() 164 165 // make sure that we log the version of mongodb as we get ready to 166 // start it 167 tlog := c.GetTestLog() 168 any := `(.|\n)*` 169 start := "^" + any 170 tail := any + "$" 171 c.Assert(tlog, gc.Matches, start+`using mongod: .*/mongod --version: "db version v2\.4\.9`+tail) 172 } 173 174 func (s *MongoSuite) TestInstallMongod(c *gc.C) { 175 type installs struct { 176 series string 177 pkg string 178 } 179 tests := []installs{ 180 {"precise", "mongodb-server"}, 181 {"quantal", "mongodb-server"}, 182 {"raring", "mongodb-server"}, 183 {"saucy", "mongodb-server"}, 184 {"trusty", "juju-mongodb"}, 185 {"u-series", "juju-mongodb"}, 186 } 187 188 mockShellCommand(c, &s.CleanupSuite, "add-apt-repository") 189 output := mockShellCommand(c, &s.CleanupSuite, "apt-get") 190 for _, test := range tests { 191 c.Logf("Testing %s", test.series) 192 dataDir := c.MkDir() 193 namespace := "namespace" + test.series 194 195 s.PatchValue(&version.Current.Series, test.series) 196 197 err := mongo.EnsureServer(dataDir, namespace, testInfo, mongo.WithHA) 198 c.Assert(err, gc.IsNil) 199 200 cmds := getMockShellCalls(c, output) 201 202 // quantal does an extra apt-get install for python software properties 203 // so we need to remember to skip that one 204 index := 0 205 if test.series == "quantal" { 206 index = 1 207 } 208 match := fmt.Sprintf(`.* install .*%s`, test.pkg) 209 c.Assert(strings.Join(cmds[index], " "), gc.Matches, match) 210 // remove the temp file between tests 211 c.Assert(os.Remove(output), gc.IsNil) 212 } 213 } 214 215 func (s *MongoSuite) TestUpstartServiceWithHA(c *gc.C) { 216 dataDir := c.MkDir() 217 218 svc, _, err := mongo.UpstartService("", dataDir, dataDir, 1234, mongo.WithHA) 219 c.Assert(err, gc.IsNil) 220 c.Assert(strings.Contains(svc.Cmd, "--replSet"), jc.IsTrue) 221 222 svc, _, err = mongo.UpstartService("", dataDir, dataDir, 1234, mongo.WithoutHA) 223 c.Assert(err, gc.IsNil) 224 c.Assert(strings.Contains(svc.Cmd, "--replSet"), jc.IsFalse) 225 } 226 227 func (s *MongoSuite) TestUpstartServiceWithJournal(c *gc.C) { 228 dataDir := c.MkDir() 229 230 svc, _, err := mongo.UpstartService("", dataDir, dataDir, 1234, mongo.WithHA) 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, mongo.WithHA) 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, mongo.WithHA) 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, mongo.WithHA) 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 := []instance.Address{{ 297 Value: "10.0.0.1", 298 Type: instance.Ipv4Address, 299 NetworkName: "cloud", 300 NetworkScope: instance.NetworkCloudLocal}, { 301 Value: "8.8.8.8", 302 Type: instance.Ipv4Address, 303 NetworkName: "public", 304 NetworkScope: instance.NetworkPublic}} 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 := []instance.HostPort{{ 313 Address: instance.Address{ 314 Value: "10.0.0.1", 315 Type: instance.Ipv4Address, 316 NetworkName: "cloud", 317 NetworkScope: instance.NetworkCloudLocal, 318 }, 319 Port: 37017}, { 320 Address: instance.Address{ 321 Value: "8.8.8.8", 322 Type: instance.Ipv4Address, 323 NetworkName: "public", 324 NetworkScope: instance.NetworkPublic, 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, mongo.WithHA) 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 }