github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"crypto/rand"
     8  	"encoding/base64"
     9  	"fmt"
    10  	"net"
    11  	"os"
    12  	"os/exec"
    13  	"path"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/replicaset"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/packaging/config"
    23  	"github.com/juju/utils/packaging/manager"
    24  	"github.com/juju/utils/series"
    25  	"gopkg.in/mgo.v2"
    26  
    27  	environs "github.com/juju/juju/environs/config"
    28  	"github.com/juju/juju/network"
    29  	"github.com/juju/juju/service"
    30  )
    31  
    32  var (
    33  	logger          = loggo.GetLogger("juju.mongo")
    34  	mongoConfigPath = "/etc/default/mongodb"
    35  
    36  	// JujuMongod24Path holds the default path to the legacy Juju
    37  	// mongod.
    38  	JujuMongod24Path = "/usr/lib/juju/bin/mongod"
    39  
    40  	// This is NUMACTL package name for apt-get
    41  	numaCtlPkg = "numactl"
    42  )
    43  
    44  // StorageEngine represents the storage used by mongo.
    45  type StorageEngine string
    46  
    47  const (
    48  	// JujuMongoPackage is the mongo package Juju uses when
    49  	// installing mongo.
    50  	JujuMongoPackage = "juju-mongodb3.2"
    51  
    52  	// MMAPV1 is the default storage engine in mongo db up to 3.x
    53  	MMAPV1 StorageEngine = "mmapv1"
    54  
    55  	// WiredTiger is a storage type introduced in 3
    56  	WiredTiger StorageEngine = "wiredTiger"
    57  
    58  	// Upgrading is a special case where mongo is being upgraded.
    59  	Upgrading StorageEngine = "Upgrading"
    60  )
    61  
    62  // Version represents the major.minor version of the runnig mongo.
    63  type Version struct {
    64  	Major         int
    65  	Minor         int
    66  	Patch         string // supports variants like 1-alpha
    67  	StorageEngine StorageEngine
    68  }
    69  
    70  // NewerThan will return 1 if the passed version is older than
    71  // v, 0 if they are equal (or ver is a special case such as
    72  // Upgrading and -1 if ver is newer.
    73  func (v Version) NewerThan(ver Version) int {
    74  	if v == MongoUpgrade || ver == MongoUpgrade {
    75  		return 0
    76  	}
    77  	if v.Major > ver.Major {
    78  		return 1
    79  	}
    80  	if v.Major < ver.Major {
    81  		return -1
    82  	}
    83  	if v.Minor > ver.Minor {
    84  		return 1
    85  	}
    86  	if v.Minor < ver.Minor {
    87  		return -1
    88  	}
    89  	return 0
    90  }
    91  
    92  // NewVersion returns a mongo Version parsing the passed version string
    93  // or error if not possible.
    94  // A valid version string is of the form:
    95  // 1.2.patch/storage
    96  // major and minor are positive integers, patch is a string containing
    97  // any ascii character except / and storage is one of the above defined
    98  // StorageEngine. Only major is mandatory.
    99  // An alternative valid string is 0.0/Upgrading which represents that
   100  // mongo is being upgraded.
   101  func NewVersion(v string) (Version, error) {
   102  	version := Version{}
   103  	if v == "" {
   104  		return Mongo24, nil
   105  	}
   106  
   107  	parts := strings.SplitN(v, "/", 2)
   108  	switch len(parts) {
   109  	case 0:
   110  		return Version{}, errors.New("invalid version string")
   111  	case 1:
   112  		version.StorageEngine = MMAPV1
   113  	case 2:
   114  		switch StorageEngine(parts[1]) {
   115  		case MMAPV1:
   116  			version.StorageEngine = MMAPV1
   117  		case WiredTiger:
   118  			version.StorageEngine = WiredTiger
   119  		case Upgrading:
   120  			version.StorageEngine = Upgrading
   121  		}
   122  	}
   123  	vParts := strings.SplitN(parts[0], ".", 3)
   124  
   125  	if len(vParts) >= 1 {
   126  		i, err := strconv.Atoi(vParts[0])
   127  		if err != nil {
   128  			return Version{}, errors.Annotate(err, "Invalid version string, major is not an int")
   129  		}
   130  		version.Major = i
   131  	}
   132  	if len(vParts) >= 2 {
   133  		i, err := strconv.Atoi(vParts[1])
   134  		if err != nil {
   135  			return Version{}, errors.Annotate(err, "Invalid version string, minor is not an int")
   136  		}
   137  		version.Minor = i
   138  	}
   139  	if len(vParts) == 3 {
   140  		version.Patch = vParts[2]
   141  	}
   142  
   143  	if version.Major == 2 && version.StorageEngine == WiredTiger {
   144  		return Version{}, errors.Errorf("Version 2.x does not support Wired Tiger storage engine")
   145  	}
   146  
   147  	// This deserialises the special "Mongo Upgrading" version
   148  	if version.Major == 0 && version.Minor == 0 {
   149  		return Version{StorageEngine: Upgrading}, nil
   150  	}
   151  
   152  	return version, nil
   153  }
   154  
   155  // String serializes the version into a string.
   156  func (v Version) String() string {
   157  	s := fmt.Sprintf("%d.%d", v.Major, v.Minor)
   158  	if v.Patch != "" {
   159  		s = fmt.Sprintf("%s.%s", s, v.Patch)
   160  	}
   161  	if v.StorageEngine != "" {
   162  		s = fmt.Sprintf("%s/%s", s, v.StorageEngine)
   163  	}
   164  	return s
   165  }
   166  
   167  // JujuMongodPath returns the path for the mongod binary
   168  // with the specified version.
   169  func JujuMongodPath(v Version) string {
   170  	return fmt.Sprintf("/usr/lib/juju/mongo%d.%d/bin/mongod", v.Major, v.Minor)
   171  }
   172  
   173  var (
   174  	// Mongo24 represents juju-mongodb 2.4.x
   175  	Mongo24 = Version{Major: 2,
   176  		Minor:         4,
   177  		Patch:         "",
   178  		StorageEngine: MMAPV1,
   179  	}
   180  	// Mongo26 represents juju-mongodb26 2.6.x
   181  	Mongo26 = Version{Major: 2,
   182  		Minor:         6,
   183  		Patch:         "",
   184  		StorageEngine: MMAPV1,
   185  	}
   186  	// Mongo32wt represents juju-mongodb3 3.2.x with wiredTiger storage.
   187  	Mongo32wt = Version{Major: 3,
   188  		Minor:         2,
   189  		Patch:         "",
   190  		StorageEngine: WiredTiger,
   191  	}
   192  	// MongoUpgrade represents a sepacial case where an upgrade is in
   193  	// progress.
   194  	MongoUpgrade = Version{Major: 0,
   195  		Minor:         0,
   196  		Patch:         "Upgrading",
   197  		StorageEngine: Upgrading,
   198  	}
   199  )
   200  
   201  // InstalledVersion returns the version of mongo installed.
   202  // We look for a specific, known version supported by this Juju,
   203  // and fall back to the original mongo 2.4.
   204  func InstalledVersion() Version {
   205  	mgoVersion := Mongo24
   206  	if binariesAvailable(Mongo32wt, os.Stat) {
   207  		mgoVersion = Mongo32wt
   208  	}
   209  	return mgoVersion
   210  }
   211  
   212  // binariesAvailable returns true if the binaries for the
   213  // given Version of mongo are available.
   214  func binariesAvailable(v Version, statFunc func(string) (os.FileInfo, error)) bool {
   215  	var path string
   216  	switch v {
   217  	case Mongo24:
   218  		// 2.4 has a fixed path.
   219  		path = JujuMongod24Path
   220  	default:
   221  		path = JujuMongodPath(v)
   222  	}
   223  	if _, err := statFunc(path); err == nil {
   224  		return true
   225  	}
   226  	return false
   227  }
   228  
   229  // WithAddresses represents an entity that has a set of
   230  // addresses. e.g. a state Machine object
   231  type WithAddresses interface {
   232  	Addresses() []network.Address
   233  }
   234  
   235  // IsMaster returns a boolean that represents whether the given
   236  // machine's peer address is the primary mongo host for the replicaset
   237  func IsMaster(session *mgo.Session, obj WithAddresses) (bool, error) {
   238  	addrs := obj.Addresses()
   239  
   240  	masterHostPort, err := replicaset.MasterHostPort(session)
   241  
   242  	// If the replica set has not been configured, then we
   243  	// can have only one master and the caller must
   244  	// be that master.
   245  	if err == replicaset.ErrMasterNotConfigured {
   246  		return true, nil
   247  	}
   248  	if err != nil {
   249  		return false, err
   250  	}
   251  
   252  	masterAddr, _, err := net.SplitHostPort(masterHostPort)
   253  	if err != nil {
   254  		return false, err
   255  	}
   256  
   257  	for _, addr := range addrs {
   258  		if addr.Value == masterAddr {
   259  			return true, nil
   260  		}
   261  	}
   262  	return false, nil
   263  }
   264  
   265  // SelectPeerAddress returns the address to use as the mongo replica set peer
   266  // address by selecting it from the given addresses. If no addresses are
   267  // available an empty string is returned.
   268  func SelectPeerAddress(addrs []network.Address) string {
   269  	logger.Debugf("selecting mongo peer address from %+v", addrs)
   270  	// ScopeMachineLocal addresses are OK if we can't pick by space, also the
   271  	// second bool return is ignored intentionally.
   272  	addr, _ := network.SelectControllerAddress(addrs, true)
   273  	return addr.Value
   274  }
   275  
   276  // SelectPeerHostPort returns the HostPort to use as the mongo replica set peer
   277  // by selecting it from the given hostPorts.
   278  func SelectPeerHostPort(hostPorts []network.HostPort) string {
   279  	logger.Debugf("selecting mongo peer hostPort by scope from %+v", hostPorts)
   280  	return network.SelectMongoHostPortsByScope(hostPorts, true)[0]
   281  }
   282  
   283  // SelectPeerHostPortBySpace returns the HostPort to use as the mongo replica set peer
   284  // by selecting it from the given hostPorts.
   285  func SelectPeerHostPortBySpace(hostPorts []network.HostPort, space network.SpaceName) string {
   286  	logger.Debugf("selecting mongo peer hostPort in space %s from %+v", space, hostPorts)
   287  	// ScopeMachineLocal addresses are OK if we can't pick by space.
   288  	suitableHostPorts, foundHostPortsInSpaces := network.SelectMongoHostPortsBySpaces(hostPorts, []network.SpaceName{space})
   289  
   290  	if !foundHostPortsInSpaces {
   291  		logger.Debugf("Failed to select hostPort by space - trying by scope from %+v", hostPorts)
   292  		suitableHostPorts = network.SelectMongoHostPortsByScope(hostPorts, true)
   293  	}
   294  	return suitableHostPorts[0]
   295  }
   296  
   297  // GenerateSharedSecret generates a pseudo-random shared secret (keyfile)
   298  // for use with Mongo replica sets.
   299  func GenerateSharedSecret() (string, error) {
   300  	// "A key’s length must be between 6 and 1024 characters and may
   301  	// only contain characters in the base64 set."
   302  	//   -- http://docs.mongodb.org/manual/tutorial/generate-key-file/
   303  	buf := make([]byte, base64.StdEncoding.DecodedLen(1024))
   304  	if _, err := rand.Read(buf); err != nil {
   305  		return "", fmt.Errorf("cannot read random secret: %v", err)
   306  	}
   307  	return base64.StdEncoding.EncodeToString(buf), nil
   308  }
   309  
   310  // Path returns the executable path to be used to run mongod on this
   311  // machine. If the juju-bundled version of mongo exists, it will return that
   312  // path, otherwise it will return the command to run mongod from the path.
   313  func Path(version Version) (string, error) {
   314  	return mongoPath(version, os.Stat, exec.LookPath)
   315  }
   316  
   317  func mongoPath(version Version, stat func(string) (os.FileInfo, error), lookPath func(string) (string, error)) (string, error) {
   318  	switch version {
   319  	case Mongo24:
   320  		if _, err := stat(JujuMongod24Path); err == nil {
   321  			return JujuMongod24Path, nil
   322  		}
   323  
   324  		path, err := lookPath("mongod")
   325  		if err != nil {
   326  			logger.Infof("could not find %v or mongod in $PATH", JujuMongod24Path)
   327  			return "", err
   328  		}
   329  		return path, nil
   330  	default:
   331  		path := JujuMongodPath(version)
   332  		var err error
   333  		if _, err = stat(path); err == nil {
   334  			return path, nil
   335  		}
   336  	}
   337  
   338  	logger.Infof("could not find a suitable binary for %q", version)
   339  	errMsg := fmt.Sprintf("no suitable binary for %q", version)
   340  	return "", errors.New(errMsg)
   341  
   342  }
   343  
   344  // EnsureServerParams is a parameter struct for EnsureServer.
   345  type EnsureServerParams struct {
   346  	// APIPort is the port to connect to the api server.
   347  	APIPort int
   348  
   349  	// StatePort is the port to connect to the mongo server.
   350  	StatePort int
   351  
   352  	// Cert is the certificate.
   353  	Cert string
   354  
   355  	// PrivateKey is the certificate's private key.
   356  	PrivateKey string
   357  
   358  	// CAPrivateKey is the CA certificate's private key.
   359  	CAPrivateKey string
   360  
   361  	// SharedSecret is a secret shared between mongo servers.
   362  	SharedSecret string
   363  
   364  	// SystemIdentity is the identity of the system.
   365  	SystemIdentity string
   366  
   367  	// DataDir is the machine agent data directory.
   368  	DataDir string
   369  
   370  	// Namespace is the machine agent's namespace, which is used to
   371  	// generate a unique service name for Mongo.
   372  	Namespace string
   373  
   374  	// OplogSize is the size of the Mongo oplog.
   375  	// If this is zero, then EnsureServer will
   376  	// calculate a default size according to the
   377  	// algorithm defined in Mongo.
   378  	OplogSize int
   379  
   380  	// SetNumaControlPolicy preference - whether the user
   381  	// wants to set the numa control policy when starting mongo.
   382  	SetNumaControlPolicy bool
   383  }
   384  
   385  // EnsureServer ensures that the MongoDB server is installed,
   386  // configured, and ready to run.
   387  //
   388  // This method will remove old versions of the mongo init service as necessary
   389  // before installing the new version.
   390  func EnsureServer(args EnsureServerParams) error {
   391  	logger.Infof(
   392  		"Ensuring mongo server is running; data directory %s; port %d",
   393  		args.DataDir, args.StatePort,
   394  	)
   395  
   396  	dbDir := filepath.Join(args.DataDir, "db")
   397  	if err := os.MkdirAll(dbDir, 0700); err != nil {
   398  		return fmt.Errorf("cannot create mongo database directory: %v", err)
   399  	}
   400  
   401  	oplogSizeMB := args.OplogSize
   402  	if oplogSizeMB == 0 {
   403  		var err error
   404  		if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
   405  			return err
   406  		}
   407  	}
   408  
   409  	operatingsystem := series.HostSeries()
   410  	if err := installMongod(operatingsystem, args.SetNumaControlPolicy); err != nil {
   411  		// This isn't treated as fatal because the Juju MongoDB
   412  		// package is likely to be already installed anyway. There
   413  		// could just be a temporary issue with apt-get/yum/whatever
   414  		// and we don't want this to stop jujud from starting.
   415  		// (LP #1441904)
   416  		logger.Errorf("cannot install/upgrade mongod (will proceed anyway): %v", err)
   417  	}
   418  	mgoVersion := InstalledVersion()
   419  	mongoPath, err := Path(mgoVersion)
   420  	if err != nil {
   421  		return err
   422  	}
   423  	logVersion(mongoPath)
   424  
   425  	if err := UpdateSSLKey(args.DataDir, args.Cert, args.PrivateKey); err != nil {
   426  		return err
   427  	}
   428  
   429  	err = utils.AtomicWriteFile(sharedSecretPath(args.DataDir), []byte(args.SharedSecret), 0600)
   430  	if err != nil {
   431  		return fmt.Errorf("cannot write mongod shared secret: %v", err)
   432  	}
   433  
   434  	// Disable the default mongodb installed by the mongodb-server package.
   435  	// Only do this if the file doesn't exist already, so users can run
   436  	// their own mongodb server if they wish to.
   437  	if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) {
   438  		err = utils.AtomicWriteFile(
   439  			mongoConfigPath,
   440  			[]byte("ENABLE_MONGODB=no"),
   441  			0644,
   442  		)
   443  		if err != nil {
   444  			return err
   445  		}
   446  	}
   447  
   448  	svcConf := newConf(args.DataDir, dbDir, mongoPath, args.StatePort, oplogSizeMB, args.SetNumaControlPolicy, mgoVersion, true)
   449  	svc, err := newService(ServiceName, svcConf)
   450  	if err != nil {
   451  		return err
   452  	}
   453  	installed, err := svc.Installed()
   454  	if err != nil {
   455  		return errors.Trace(err)
   456  	}
   457  	if installed {
   458  		exists, err := svc.Exists()
   459  		if err != nil {
   460  			return errors.Trace(err)
   461  		}
   462  		if exists {
   463  			logger.Debugf("mongo exists as expected")
   464  			running, err := svc.Running()
   465  			if err != nil {
   466  				return errors.Trace(err)
   467  			}
   468  			if !running {
   469  				return svc.Start()
   470  			}
   471  			return nil
   472  		}
   473  	}
   474  
   475  	if err := svc.Stop(); err != nil {
   476  		return errors.Annotatef(err, "failed to stop mongo")
   477  	}
   478  	if err := makeJournalDirs(dbDir); err != nil {
   479  		return fmt.Errorf("error creating journal directories: %v", err)
   480  	}
   481  	if err := preallocOplog(dbDir, oplogSizeMB); err != nil {
   482  		return fmt.Errorf("error creating oplog files: %v", err)
   483  	}
   484  	if err := service.InstallAndStart(svc); err != nil {
   485  		return errors.Trace(err)
   486  	}
   487  	return nil
   488  }
   489  
   490  // UpdateSSLKey writes a new SSL key used by mongo to validate connections from Juju controller(s)
   491  func UpdateSSLKey(dataDir, cert, privateKey string) error {
   492  	certKey := cert + "\n" + privateKey
   493  	err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600)
   494  	return errors.Annotate(err, "cannot write SSL key")
   495  }
   496  
   497  func makeJournalDirs(dataDir string) error {
   498  	journalDir := path.Join(dataDir, "journal")
   499  	if err := os.MkdirAll(journalDir, 0700); err != nil {
   500  		logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err)
   501  		return err
   502  	}
   503  
   504  	// Manually create the prealloc files, since otherwise they get
   505  	// created as 100M files. We create three files of 1MB each.
   506  	prefix := filepath.Join(journalDir, "prealloc.")
   507  	preallocSize := 1024 * 1024
   508  	return preallocFiles(prefix, preallocSize, preallocSize, preallocSize)
   509  }
   510  
   511  func logVersion(mongoPath string) {
   512  	cmd := exec.Command(mongoPath, "--version")
   513  	output, err := cmd.CombinedOutput()
   514  	if err != nil {
   515  		logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
   516  		return
   517  	}
   518  	logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
   519  }
   520  
   521  func installPackage(pkg string, pacconfer config.PackagingConfigurer, pacman manager.PackageManager) error {
   522  	// apply release targeting if needed.
   523  	if pacconfer.IsCloudArchivePackage(pkg) {
   524  		pkg = strings.Join(pacconfer.ApplyCloudArchiveTarget(pkg), " ")
   525  	}
   526  
   527  	return pacman.Install(pkg)
   528  }
   529  
   530  func installMongod(operatingsystem string, numaCtl bool) error {
   531  	// fetch the packaging configuration manager for the current operating system.
   532  	pacconfer, err := config.NewPackagingConfigurer(operatingsystem)
   533  	if err != nil {
   534  		return err
   535  	}
   536  
   537  	// fetch the package manager implementation for the current operating system.
   538  	pacman, err := manager.NewPackageManager(operatingsystem)
   539  	if err != nil {
   540  		return err
   541  	}
   542  
   543  	// CentOS requires "epel-release" for the epel repo mongodb-server is in.
   544  	if operatingsystem == "centos7" {
   545  		// install epel-release
   546  		if err := pacman.Install("epel-release"); err != nil {
   547  			return err
   548  		}
   549  	}
   550  
   551  	mongoPkgs, fallbackPkgs := packagesForSeries(operatingsystem)
   552  
   553  	if numaCtl {
   554  		logger.Infof("installing %v and %s", mongoPkgs, numaCtlPkg)
   555  		if err = installPackage(numaCtlPkg, pacconfer, pacman); err != nil {
   556  			return errors.Trace(err)
   557  		}
   558  	} else {
   559  		logger.Infof("installing %v", mongoPkgs)
   560  	}
   561  
   562  	for i := range mongoPkgs {
   563  		if err = installPackage(mongoPkgs[i], pacconfer, pacman); err != nil {
   564  			break
   565  		}
   566  	}
   567  	if err != nil && len(fallbackPkgs) == 0 {
   568  		return errors.Trace(err)
   569  	}
   570  	if err != nil {
   571  		logger.Errorf("installing mongo failed: %v", err)
   572  		logger.Infof("will try fallback packages %v", fallbackPkgs)
   573  		for i := range fallbackPkgs {
   574  			if err = installPackage(fallbackPkgs[i], pacconfer, pacman); err != nil {
   575  				return errors.Trace(err)
   576  			}
   577  		}
   578  	}
   579  
   580  	// Work around SELinux on centos7
   581  	if operatingsystem == "centos7" {
   582  		cmd := []string{"chcon", "-R", "-v", "-t", "mongod_var_lib_t", "/var/lib/juju/"}
   583  		logger.Infof("running %s %v", cmd[0], cmd[1:])
   584  		_, err = utils.RunCommand(cmd[0], cmd[1:]...)
   585  		if err != nil {
   586  			logger.Errorf("chcon failed to change file security context error %s", err)
   587  			return err
   588  		}
   589  
   590  		cmd = []string{"semanage", "port", "-a", "-t", "mongod_port_t", "-p", "tcp", strconv.Itoa(environs.DefaultStatePort)}
   591  		logger.Infof("running %s %v", cmd[0], cmd[1:])
   592  		_, err = utils.RunCommand(cmd[0], cmd[1:]...)
   593  		if err != nil {
   594  			if !strings.Contains(err.Error(), "exit status 1") {
   595  				logger.Errorf("semanage failed to provide access on port %d error %s", environs.DefaultStatePort, err)
   596  				return err
   597  			}
   598  		}
   599  	}
   600  
   601  	return nil
   602  }
   603  
   604  // packagesForSeries returns the name of the mongo package for the series
   605  // of the machine that it is going to be running on plus a fallback for
   606  // options where the package is going to be ready eventually but might not
   607  // yet be.
   608  func packagesForSeries(series string) ([]string, []string) {
   609  	switch series {
   610  	case "precise", "quantal", "raring", "saucy", "centos7":
   611  		return []string{"mongodb-server"}, []string{}
   612  	case "trusty", "wily", "xenial":
   613  		return []string{JujuMongoPackage}, []string{"juju-mongodb"}
   614  	default:
   615  		// y and onwards
   616  		return []string{JujuMongoPackage}, []string{}
   617  	}
   618  }
   619  
   620  // DbDir returns the dir where mongo storage is.
   621  func DbDir(dataDir string) string {
   622  	return filepath.Join(dataDir, "db")
   623  }