github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/agent/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/instance"
    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.agent.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() []instance.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 []instance.Address) string {
    93  	return instance.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 []instance.HostPort) string {
    99  	return instance.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  const (
   138  	// WithHA is used when we want to start a mongo service with HA support.
   139  	WithHA = true
   140  	// WithoutHA is used when we want to start a mongo service without HA support.
   141  	WithoutHA = false
   142  )
   143  
   144  // EnsureMongoServer ensures that the correct mongo upstart script is installed
   145  // and running.
   146  //
   147  // This method will remove old versions of the mongo upstart script as necessary
   148  // before installing the new version.
   149  //
   150  // The namespace is a unique identifier to prevent multiple instances of mongo
   151  // on this machine from colliding. This should be empty unless using
   152  // the local provider.
   153  func EnsureServer(dataDir string, namespace string, info params.StateServingInfo, withHA bool) error {
   154  	logger.Infof("Ensuring mongo server is running; data directory %s; port %d", dataDir, info.StatePort)
   155  	dbDir := filepath.Join(dataDir, "db")
   156  
   157  	if err := os.MkdirAll(dbDir, 0700); err != nil {
   158  		return fmt.Errorf("cannot create mongo database directory: %v", err)
   159  	}
   160  
   161  	certKey := info.Cert + "\n" + info.PrivateKey
   162  	err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600)
   163  	if err != nil {
   164  		return fmt.Errorf("cannot write SSL key: %v", err)
   165  	}
   166  
   167  	err = utils.AtomicWriteFile(sharedSecretPath(dataDir), []byte(info.SharedSecret), 0600)
   168  	if err != nil {
   169  		return fmt.Errorf("cannot write mongod shared secret: %v", err)
   170  	}
   171  
   172  	// Disable the default mongodb installed by the mongodb-server package.
   173  	// Only do this if the file doesn't exist already, so users can run
   174  	// their own mongodb server if they wish to.
   175  	if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) {
   176  		err = utils.AtomicWriteFile(
   177  			mongoConfigPath,
   178  			[]byte("ENABLE_MONGODB=no"),
   179  			0644,
   180  		)
   181  		if err != nil {
   182  			return err
   183  		}
   184  	}
   185  
   186  	if err := aptGetInstallMongod(); err != nil {
   187  		return fmt.Errorf("cannot install mongod: %v", err)
   188  	}
   189  
   190  	upstartConf, mongoPath, err := upstartService(namespace, dataDir, dbDir, info.StatePort, withHA)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	logVersion(mongoPath)
   195  
   196  	if err := upstartServiceStop(&upstartConf.Service); err != nil {
   197  		return fmt.Errorf("failed to stop mongo: %v", err)
   198  	}
   199  	if err := makeJournalDirs(dbDir); err != nil {
   200  		return fmt.Errorf("error creating journal directories: %v", err)
   201  	}
   202  	return upstartConfInstall(upstartConf)
   203  }
   204  
   205  // ServiceName returns the name of the upstart service config for mongo using
   206  // the given namespace.
   207  func ServiceName(namespace string) string {
   208  	if namespace != "" {
   209  		return fmt.Sprintf("%s-%s", serviceName, namespace)
   210  	}
   211  	return serviceName
   212  }
   213  
   214  func makeJournalDirs(dataDir string) error {
   215  	journalDir := path.Join(dataDir, "journal")
   216  
   217  	if err := os.MkdirAll(journalDir, 0700); err != nil {
   218  		logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err)
   219  		return err
   220  	}
   221  
   222  	// manually create the prealloc files, since otherwise they get created as 100M files.
   223  	zeroes := make([]byte, 64*1024) // should be enough for anyone
   224  	for x := 0; x < 3; x++ {
   225  		name := fmt.Sprintf("prealloc.%d", x)
   226  		filename := filepath.Join(journalDir, name)
   227  		f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700)
   228  		// TODO(jam) 2014-04-12 https://launchpad.net/bugs/1306902
   229  		// When we support upgrading Mongo into Replica mode, we should
   230  		// start rewriting the upstart config
   231  		if os.IsExist(err) {
   232  			// already exists, don't overwrite
   233  			continue
   234  		}
   235  		if err != nil {
   236  			return fmt.Errorf("failed to open mongo prealloc file %q: %v", filename, err)
   237  		}
   238  		defer f.Close()
   239  		for total := 0; total < 1024*1024; {
   240  			n, err := f.Write(zeroes)
   241  			if err != nil {
   242  				return fmt.Errorf("failed to write to mongo prealloc file %q: %v", filename, err)
   243  			}
   244  			total += n
   245  		}
   246  	}
   247  	return nil
   248  }
   249  
   250  func logVersion(mongoPath string) {
   251  	cmd := exec.Command(mongoPath, "--version")
   252  	output, err := cmd.CombinedOutput()
   253  	if err != nil {
   254  		logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
   255  		return
   256  	}
   257  	logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
   258  }
   259  
   260  func sslKeyPath(dataDir string) string {
   261  	return filepath.Join(dataDir, "server.pem")
   262  }
   263  
   264  func sharedSecretPath(dataDir string) string {
   265  	return filepath.Join(dataDir, SharedSecretFile)
   266  }
   267  
   268  // upstartService returns the upstart config for the mongo state service.
   269  // It also returns the path to the mongod executable that the upstart config
   270  // will be using.
   271  func upstartService(namespace, dataDir, dbDir string, port int, withHA bool) (*upstart.Conf, string, error) {
   272  	svc := upstart.NewService(ServiceName(namespace))
   273  
   274  	mongoPath, err := Path()
   275  	if err != nil {
   276  		return nil, "", err
   277  	}
   278  
   279  	mongoCmd := mongoPath + " --auth" +
   280  		" --dbpath=" + utils.ShQuote(dbDir) +
   281  		" --sslOnNormalPorts" +
   282  		" --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(dataDir)) +
   283  		" --sslPEMKeyPassword ignored" +
   284  		" --bind_ip 0.0.0.0" +
   285  		" --port " + fmt.Sprint(port) +
   286  		" --noprealloc" +
   287  		" --syslog" +
   288  		" --smallfiles" +
   289  		" --journal" +
   290  		" --keyFile " + utils.ShQuote(sharedSecretPath(dataDir))
   291  	if withHA {
   292  		mongoCmd += " --replSet " + ReplicaSetName
   293  	}
   294  	conf := &upstart.Conf{
   295  		Service: *svc,
   296  		Desc:    "juju state database",
   297  		Limit: map[string]string{
   298  			"nofile": fmt.Sprintf("%d %d", maxFiles, maxFiles),
   299  			"nproc":  fmt.Sprintf("%d %d", maxProcs, maxProcs),
   300  		},
   301  		Cmd: mongoCmd,
   302  	}
   303  	return conf, mongoPath, nil
   304  }
   305  
   306  func aptGetInstallMongod() error {
   307  	// Only Quantal requires the PPA.
   308  	if version.Current.Series == "quantal" {
   309  		if err := addAptRepository("ppa:juju/stable"); err != nil {
   310  			return err
   311  		}
   312  	}
   313  	pkg := packageForSeries(version.Current.Series)
   314  	cmds := apt.GetPreparePackages([]string{pkg}, version.Current.Series)
   315  	logger.Infof("installing %s", pkg)
   316  	for _, cmd := range cmds {
   317  		if err := apt.GetInstall(cmd...); err != nil {
   318  			return err
   319  		}
   320  	}
   321  	return nil
   322  }
   323  
   324  func addAptRepository(name string) error {
   325  	// add-apt-repository requires python-software-properties
   326  	cmds := apt.GetPreparePackages(
   327  		[]string{"python-software-properties"},
   328  		version.Current.Series,
   329  	)
   330  	logger.Infof("installing python-software-properties")
   331  	for _, cmd := range cmds {
   332  		if err := apt.GetInstall(cmd...); err != nil {
   333  			return err
   334  		}
   335  	}
   336  
   337  	logger.Infof("adding apt repository %q", name)
   338  	cmd := exec.Command("add-apt-repository", "-y", name)
   339  	out, err := cmd.CombinedOutput()
   340  	if err != nil {
   341  		return fmt.Errorf("cannot add apt repository: %v (output %s)", err, bytes.TrimSpace(out))
   342  	}
   343  	return nil
   344  }
   345  
   346  // packageForSeries returns the name of the mongo package for the series
   347  // of the machine that it is going to be running on.
   348  func packageForSeries(series string) string {
   349  	switch series {
   350  	case "precise", "quantal", "raring", "saucy":
   351  		return "mongodb-server"
   352  	default:
   353  		// trusty and onwards
   354  		return "juju-mongodb"
   355  	}
   356  }
   357  
   358  // noauthCommand returns an os/exec.Cmd that may be executed to
   359  // run mongod without security.
   360  func noauthCommand(dataDir string, port int) (*exec.Cmd, error) {
   361  	sslKeyFile := path.Join(dataDir, "server.pem")
   362  	dbDir := filepath.Join(dataDir, "db")
   363  	mongoPath, err := Path()
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  	cmd := exec.Command(mongoPath,
   368  		"--noauth",
   369  		"--dbpath", dbDir,
   370  		"--sslOnNormalPorts",
   371  		"--sslPEMKeyFile", sslKeyFile,
   372  		"--sslPEMKeyPassword", "ignored",
   373  		"--bind_ip", "127.0.0.1",
   374  		"--port", fmt.Sprint(port),
   375  		"--noprealloc",
   376  		"--syslog",
   377  		"--smallfiles",
   378  		"--journal",
   379  	)
   380  	return cmd, nil
   381  }