github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/mongo/mongo.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package mongo
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/rand"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"net"
    12  	"os"
    13  	"os/exec"
    14  	"path"
    15  	"path/filepath"
    16  
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/utils"
    19  	"github.com/juju/utils/apt"
    20  	"labix.org/v2/mgo"
    21  
    22  	"github.com/juju/juju/network"
    23  	"github.com/juju/juju/replicaset"
    24  	"github.com/juju/juju/state/api/params"
    25  	"github.com/juju/juju/upstart"
    26  	"github.com/juju/juju/version"
    27  )
    28  
    29  const (
    30  	maxFiles = 65000
    31  	maxProcs = 20000
    32  
    33  	serviceName = "juju-db"
    34  
    35  	// SharedSecretFile is the name of the Mongo shared secret file
    36  	// located within the Juju data directory.
    37  	SharedSecretFile = "shared-secret"
    38  
    39  	// ReplicaSetName is the name of the replica set that juju uses for its
    40  	// state servers.
    41  	ReplicaSetName = "juju"
    42  )
    43  
    44  var (
    45  	logger          = loggo.GetLogger("juju.mongo")
    46  	mongoConfigPath = "/etc/default/mongodb"
    47  
    48  	// JujuMongodPath holds the default path to the juju-specific mongod.
    49  	JujuMongodPath = "/usr/lib/juju/bin/mongod"
    50  
    51  	upstartConfInstall          = (*upstart.Conf).Install
    52  	upstartServiceStopAndRemove = (*upstart.Service).StopAndRemove
    53  	upstartServiceStop          = (*upstart.Service).Stop
    54  	upstartServiceStart         = (*upstart.Service).Start
    55  )
    56  
    57  // WithAddresses represents an entity that has a set of
    58  // addresses. e.g. a state Machine object
    59  type WithAddresses interface {
    60  	Addresses() []network.Address
    61  }
    62  
    63  // IsMaster returns a boolean that represents whether the given
    64  // machine's peer address is the primary mongo host for the replicaset
    65  func IsMaster(session *mgo.Session, obj WithAddresses) (bool, error) {
    66  	addrs := obj.Addresses()
    67  
    68  	masterHostPort, err := replicaset.MasterHostPort(session)
    69  
    70  	// If the replica set has not been configured, then we
    71  	// can have only one master and the caller must
    72  	// be that master.
    73  	if err == replicaset.ErrMasterNotConfigured {
    74  		return true, nil
    75  	}
    76  
    77  	if err != nil {
    78  		return false, err
    79  	}
    80  
    81  	masterAddr, _, err := net.SplitHostPort(masterHostPort)
    82  	if err != nil {
    83  		return false, err
    84  	}
    85  
    86  	machinePeerAddr := SelectPeerAddress(addrs)
    87  	return machinePeerAddr == masterAddr, nil
    88  }
    89  
    90  // SelectPeerAddress returns the address to use as the
    91  // mongo replica set peer address by selecting it from the given addresses.
    92  func SelectPeerAddress(addrs []network.Address) string {
    93  	return network.SelectInternalAddress(addrs, false)
    94  }
    95  
    96  // SelectPeerHostPort returns the HostPort to use as the
    97  // mongo replica set peer by selecting it from the given hostPorts.
    98  func SelectPeerHostPort(hostPorts []network.HostPort) string {
    99  	return network.SelectInternalHostPort(hostPorts, false)
   100  }
   101  
   102  // GenerateSharedSecret generates a pseudo-random shared secret (keyfile)
   103  // for use with Mongo replica sets.
   104  func GenerateSharedSecret() (string, error) {
   105  	// "A key’s length must be between 6 and 1024 characters and may
   106  	// only contain characters in the base64 set."
   107  	//   -- http://docs.mongodb.org/manual/tutorial/generate-key-file/
   108  	buf := make([]byte, base64.StdEncoding.DecodedLen(1024))
   109  	if _, err := rand.Read(buf); err != nil {
   110  		return "", fmt.Errorf("cannot read random secret: %v", err)
   111  	}
   112  	return base64.StdEncoding.EncodeToString(buf), nil
   113  }
   114  
   115  // Path returns the executable path to be used to run mongod on this
   116  // machine. If the juju-bundled version of mongo exists, it will return that
   117  // path, otherwise it will return the command to run mongod from the path.
   118  func Path() (string, error) {
   119  	if _, err := os.Stat(JujuMongodPath); err == nil {
   120  		return JujuMongodPath, nil
   121  	}
   122  
   123  	path, err := exec.LookPath("mongod")
   124  	if err != nil {
   125  		logger.Infof("could not find %v or mongod in $PATH", JujuMongodPath)
   126  		return "", err
   127  	}
   128  	return path, nil
   129  }
   130  
   131  // RemoveService removes the mongoDB upstart service from this machine.
   132  func RemoveService(namespace string) error {
   133  	svc := upstart.NewService(ServiceName(namespace))
   134  	return upstartServiceStopAndRemove(svc)
   135  }
   136  
   137  // EnsureMongoServer ensures that the correct mongo upstart script is installed
   138  // and running.
   139  //
   140  // This method will remove old versions of the mongo upstart script as necessary
   141  // before installing the new version.
   142  //
   143  // The namespace is a unique identifier to prevent multiple instances of mongo
   144  // on this machine from colliding. This should be empty unless using
   145  // the local provider.
   146  func EnsureServer(dataDir string, namespace string, info params.StateServingInfo) error {
   147  	logger.Infof("Ensuring mongo server is running; data directory %s; port %d", dataDir, info.StatePort)
   148  	dbDir := filepath.Join(dataDir, "db")
   149  
   150  	if err := os.MkdirAll(dbDir, 0700); err != nil {
   151  		return fmt.Errorf("cannot create mongo database directory: %v", err)
   152  	}
   153  
   154  	certKey := info.Cert + "\n" + info.PrivateKey
   155  	err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600)
   156  	if err != nil {
   157  		return fmt.Errorf("cannot write SSL key: %v", err)
   158  	}
   159  
   160  	err = utils.AtomicWriteFile(sharedSecretPath(dataDir), []byte(info.SharedSecret), 0600)
   161  	if err != nil {
   162  		return fmt.Errorf("cannot write mongod shared secret: %v", err)
   163  	}
   164  
   165  	// Disable the default mongodb installed by the mongodb-server package.
   166  	// Only do this if the file doesn't exist already, so users can run
   167  	// their own mongodb server if they wish to.
   168  	if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) {
   169  		err = utils.AtomicWriteFile(
   170  			mongoConfigPath,
   171  			[]byte("ENABLE_MONGODB=no"),
   172  			0644,
   173  		)
   174  		if err != nil {
   175  			return err
   176  		}
   177  	}
   178  
   179  	if err := aptGetInstallMongod(); err != nil {
   180  		return fmt.Errorf("cannot install mongod: %v", err)
   181  	}
   182  
   183  	upstartConf, mongoPath, err := upstartService(namespace, dataDir, dbDir, info.StatePort)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	logVersion(mongoPath)
   188  
   189  	if err := upstartServiceStop(&upstartConf.Service); err != nil {
   190  		return fmt.Errorf("failed to stop mongo: %v", err)
   191  	}
   192  	if err := makeJournalDirs(dbDir); err != nil {
   193  		return fmt.Errorf("error creating journal directories: %v", err)
   194  	}
   195  	if err := preallocOplog(dbDir); err != nil {
   196  		return fmt.Errorf("error creating oplog files: %v", err)
   197  	}
   198  	return upstartConfInstall(upstartConf)
   199  }
   200  
   201  // ServiceName returns the name of the upstart service config for mongo using
   202  // the given namespace.
   203  func ServiceName(namespace string) string {
   204  	if namespace != "" {
   205  		return fmt.Sprintf("%s-%s", serviceName, namespace)
   206  	}
   207  	return serviceName
   208  }
   209  
   210  func makeJournalDirs(dataDir string) error {
   211  	journalDir := path.Join(dataDir, "journal")
   212  	if err := os.MkdirAll(journalDir, 0700); err != nil {
   213  		logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err)
   214  		return err
   215  	}
   216  
   217  	// Manually create the prealloc files, since otherwise they get
   218  	// created as 100M files. We create three files of 1MB each.
   219  	prefix := filepath.Join(journalDir, "prealloc.")
   220  	preallocSize := 1024 * 1024
   221  	return preallocFiles(prefix, preallocSize, preallocSize, preallocSize)
   222  }
   223  
   224  func logVersion(mongoPath string) {
   225  	cmd := exec.Command(mongoPath, "--version")
   226  	output, err := cmd.CombinedOutput()
   227  	if err != nil {
   228  		logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
   229  		return
   230  	}
   231  	logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
   232  }
   233  
   234  func sslKeyPath(dataDir string) string {
   235  	return filepath.Join(dataDir, "server.pem")
   236  }
   237  
   238  func sharedSecretPath(dataDir string) string {
   239  	return filepath.Join(dataDir, SharedSecretFile)
   240  }
   241  
   242  // upstartService returns the upstart config for the mongo state service.
   243  // It also returns the path to the mongod executable that the upstart config
   244  // will be using.
   245  func upstartService(namespace, dataDir, dbDir string, port int) (*upstart.Conf, string, error) {
   246  	svc := upstart.NewService(ServiceName(namespace))
   247  
   248  	mongoPath, err := Path()
   249  	if err != nil {
   250  		return nil, "", err
   251  	}
   252  
   253  	mongoCmd := mongoPath + " --auth" +
   254  		" --dbpath=" + utils.ShQuote(dbDir) +
   255  		" --sslOnNormalPorts" +
   256  		" --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(dataDir)) +
   257  		" --sslPEMKeyPassword ignored" +
   258  		" --bind_ip 0.0.0.0" +
   259  		" --port " + fmt.Sprint(port) +
   260  		" --noprealloc" +
   261  		" --syslog" +
   262  		" --smallfiles" +
   263  		" --journal" +
   264  		" --keyFile " + utils.ShQuote(sharedSecretPath(dataDir)) +
   265  		" --replSet " + ReplicaSetName
   266  	conf := &upstart.Conf{
   267  		Service: *svc,
   268  		Desc:    "juju state database",
   269  		Limit: map[string]string{
   270  			"nofile": fmt.Sprintf("%d %d", maxFiles, maxFiles),
   271  			"nproc":  fmt.Sprintf("%d %d", maxProcs, maxProcs),
   272  		},
   273  		Cmd: mongoCmd,
   274  	}
   275  	return conf, mongoPath, nil
   276  }
   277  
   278  func aptGetInstallMongod() error {
   279  	// Only Quantal requires the PPA.
   280  	if version.Current.Series == "quantal" {
   281  		if err := addAptRepository("ppa:juju/stable"); err != nil {
   282  			return err
   283  		}
   284  	}
   285  	pkg := packageForSeries(version.Current.Series)
   286  	cmds := apt.GetPreparePackages([]string{pkg}, version.Current.Series)
   287  	logger.Infof("installing %s", pkg)
   288  	for _, cmd := range cmds {
   289  		if err := apt.GetInstall(cmd...); err != nil {
   290  			return err
   291  		}
   292  	}
   293  	return nil
   294  }
   295  
   296  func addAptRepository(name string) error {
   297  	// add-apt-repository requires python-software-properties
   298  	cmds := apt.GetPreparePackages(
   299  		[]string{"python-software-properties"},
   300  		version.Current.Series,
   301  	)
   302  	logger.Infof("installing python-software-properties")
   303  	for _, cmd := range cmds {
   304  		if err := apt.GetInstall(cmd...); err != nil {
   305  			return err
   306  		}
   307  	}
   308  
   309  	logger.Infof("adding apt repository %q", name)
   310  	cmd := exec.Command("add-apt-repository", "-y", name)
   311  	out, err := cmd.CombinedOutput()
   312  	if err != nil {
   313  		return fmt.Errorf("cannot add apt repository: %v (output %s)", err, bytes.TrimSpace(out))
   314  	}
   315  	return nil
   316  }
   317  
   318  // packageForSeries returns the name of the mongo package for the series
   319  // of the machine that it is going to be running on.
   320  func packageForSeries(series string) string {
   321  	switch series {
   322  	case "precise", "quantal", "raring", "saucy":
   323  		return "mongodb-server"
   324  	default:
   325  		// trusty and onwards
   326  		return "juju-mongodb"
   327  	}
   328  }
   329  
   330  // noauthCommand returns an os/exec.Cmd that may be executed to
   331  // run mongod without security.
   332  func noauthCommand(dataDir string, port int) (*exec.Cmd, error) {
   333  	sslKeyFile := path.Join(dataDir, "server.pem")
   334  	dbDir := filepath.Join(dataDir, "db")
   335  	mongoPath, err := Path()
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	cmd := exec.Command(mongoPath,
   340  		"--noauth",
   341  		"--dbpath", dbDir,
   342  		"--sslOnNormalPorts",
   343  		"--sslPEMKeyFile", sslKeyFile,
   344  		"--sslPEMKeyPassword", "ignored",
   345  		"--bind_ip", "127.0.0.1",
   346  		"--port", fmt.Sprint(port),
   347  		"--noprealloc",
   348  		"--syslog",
   349  		"--smallfiles",
   350  		"--journal",
   351  	)
   352  	return cmd, nil
   353  }