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  }