github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/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  	"strings"
    16  
    17  	"github.com/juju/errors"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/replicaset"
    20  	"github.com/juju/utils"
    21  	"github.com/juju/utils/packaging/config"
    22  	"github.com/juju/utils/packaging/manager"
    23  	"gopkg.in/mgo.v2"
    24  
    25  	"github.com/juju/juju/network"
    26  	"github.com/juju/juju/service"
    27  	"github.com/juju/juju/version"
    28  )
    29  
    30  var (
    31  	logger          = loggo.GetLogger("juju.mongo")
    32  	mongoConfigPath = "/etc/default/mongodb"
    33  
    34  	// JujuMongodPath holds the default path to the juju-specific
    35  	// mongod.
    36  	JujuMongodPath = "/usr/lib/juju/bin/mongod"
    37  
    38  	// This is NUMACTL package name for apt-get
    39  	numaCtlPkg = "numactl"
    40  )
    41  
    42  // WithAddresses represents an entity that has a set of
    43  // addresses. e.g. a state Machine object
    44  type WithAddresses interface {
    45  	Addresses() []network.Address
    46  }
    47  
    48  // IsMaster returns a boolean that represents whether the given
    49  // machine's peer address is the primary mongo host for the replicaset
    50  func IsMaster(session *mgo.Session, obj WithAddresses) (bool, error) {
    51  	addrs := obj.Addresses()
    52  
    53  	masterHostPort, err := replicaset.MasterHostPort(session)
    54  
    55  	// If the replica set has not been configured, then we
    56  	// can have only one master and the caller must
    57  	// be that master.
    58  	if err == replicaset.ErrMasterNotConfigured {
    59  		return true, nil
    60  	}
    61  	if err != nil {
    62  		return false, err
    63  	}
    64  
    65  	masterAddr, _, err := net.SplitHostPort(masterHostPort)
    66  	if err != nil {
    67  		return false, err
    68  	}
    69  
    70  	for _, addr := range addrs {
    71  		if addr.Value == masterAddr {
    72  			return true, nil
    73  		}
    74  	}
    75  	return false, nil
    76  }
    77  
    78  // SelectPeerAddress returns the address to use as the
    79  // mongo replica set peer address by selecting it from the given addresses.
    80  func SelectPeerAddress(addrs []network.Address) string {
    81  	return network.SelectInternalAddress(addrs, false)
    82  }
    83  
    84  // SelectPeerHostPort returns the HostPort to use as the
    85  // mongo replica set peer by selecting it from the given hostPorts.
    86  func SelectPeerHostPort(hostPorts []network.HostPort) string {
    87  	return network.SelectInternalHostPort(hostPorts, false)
    88  }
    89  
    90  // GenerateSharedSecret generates a pseudo-random shared secret (keyfile)
    91  // for use with Mongo replica sets.
    92  func GenerateSharedSecret() (string, error) {
    93  	// "A key’s length must be between 6 and 1024 characters and may
    94  	// only contain characters in the base64 set."
    95  	//   -- http://docs.mongodb.org/manual/tutorial/generate-key-file/
    96  	buf := make([]byte, base64.StdEncoding.DecodedLen(1024))
    97  	if _, err := rand.Read(buf); err != nil {
    98  		return "", fmt.Errorf("cannot read random secret: %v", err)
    99  	}
   100  	return base64.StdEncoding.EncodeToString(buf), nil
   101  }
   102  
   103  // Path returns the executable path to be used to run mongod on this
   104  // machine. If the juju-bundled version of mongo exists, it will return that
   105  // path, otherwise it will return the command to run mongod from the path.
   106  func Path() (string, error) {
   107  	if _, err := os.Stat(JujuMongodPath); err == nil {
   108  		return JujuMongodPath, nil
   109  	}
   110  
   111  	path, err := exec.LookPath("mongod")
   112  	if err != nil {
   113  		logger.Infof("could not find %v or mongod in $PATH", JujuMongodPath)
   114  		return "", err
   115  	}
   116  	return path, nil
   117  }
   118  
   119  // EnsureServerParams is a parameter struct for EnsureServer.
   120  type EnsureServerParams struct {
   121  	// APIPort is the port to connect to the api server.
   122  	APIPort int
   123  
   124  	// StatePort is the port to connect to the mongo server.
   125  	StatePort int
   126  
   127  	// Cert is the certificate.
   128  	Cert string
   129  
   130  	// PrivateKey is the certificate's private key.
   131  	PrivateKey string
   132  
   133  	// CAPrivateKey is the CA certificate's private key.
   134  	CAPrivateKey string
   135  
   136  	// SharedSecret is a secret shared between mongo servers.
   137  	SharedSecret string
   138  
   139  	// SystemIdentity is the identity of the system.
   140  	SystemIdentity string
   141  
   142  	// DataDir is the machine agent data directory.
   143  	DataDir string
   144  
   145  	// Namespace is the machine agent's namespace, which is used to
   146  	// generate a unique service name for Mongo.
   147  	Namespace string
   148  
   149  	// OplogSize is the size of the Mongo oplog.
   150  	// If this is zero, then EnsureServer will
   151  	// calculate a default size according to the
   152  	// algorithm defined in Mongo.
   153  	OplogSize int
   154  
   155  	// SetNumaControlPolicy preference - whether the user
   156  	// wants to set the numa control policy when starting mongo.
   157  	SetNumaControlPolicy bool
   158  }
   159  
   160  // EnsureServer ensures that the MongoDB server is installed,
   161  // configured, and ready to run.
   162  //
   163  // This method will remove old versions of the mongo init service as necessary
   164  // before installing the new version.
   165  //
   166  // The namespace is a unique identifier to prevent multiple instances of mongo
   167  // on this machine from colliding. This should be empty unless using
   168  // the local provider.
   169  func EnsureServer(args EnsureServerParams) error {
   170  	logger.Infof(
   171  		"Ensuring mongo server is running; data directory %s; port %d",
   172  		args.DataDir, args.StatePort,
   173  	)
   174  
   175  	dbDir := filepath.Join(args.DataDir, "db")
   176  	if err := os.MkdirAll(dbDir, 0700); err != nil {
   177  		return fmt.Errorf("cannot create mongo database directory: %v", err)
   178  	}
   179  
   180  	oplogSizeMB := args.OplogSize
   181  	if oplogSizeMB == 0 {
   182  		var err error
   183  		if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
   184  			return err
   185  		}
   186  	}
   187  
   188  	if err := installMongod(args.SetNumaControlPolicy); err != nil {
   189  		// This isn't treated as fatal because the Juju MongoDB
   190  		// package is likely to be already installed anyway. There
   191  		// could just be a temporary issue with apt-get/yum/whatever
   192  		// and we don't want this to stop jujud from starting.
   193  		// (LP #1441904)
   194  		logger.Errorf("cannot install/upgrade mongod (will proceed anyway): %v", err)
   195  	}
   196  	mongoPath, err := Path()
   197  	if err != nil {
   198  		return err
   199  	}
   200  	logVersion(mongoPath)
   201  
   202  	svcConf := newConf(args.DataDir, dbDir, mongoPath, args.StatePort, oplogSizeMB, args.SetNumaControlPolicy)
   203  	svc, err := newService(ServiceName(args.Namespace), svcConf)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	installed, err := svc.Installed()
   208  	if err != nil {
   209  		return errors.Trace(err)
   210  	}
   211  	if installed {
   212  		exists, err := svc.Exists()
   213  		if err != nil {
   214  			return errors.Trace(err)
   215  		}
   216  		if exists {
   217  			logger.Debugf("mongo exists as expected")
   218  			running, err := svc.Running()
   219  			if err != nil {
   220  				return errors.Trace(err)
   221  			}
   222  			if !running {
   223  				return svc.Start()
   224  			}
   225  			return nil
   226  		}
   227  	}
   228  
   229  	if err := UpdateSSLKey(args.DataDir, args.Cert, args.PrivateKey); err != nil {
   230  		return err
   231  	}
   232  
   233  	err = utils.AtomicWriteFile(sharedSecretPath(args.DataDir), []byte(args.SharedSecret), 0600)
   234  	if err != nil {
   235  		return fmt.Errorf("cannot write mongod shared secret: %v", err)
   236  	}
   237  
   238  	// Disable the default mongodb installed by the mongodb-server package.
   239  	// Only do this if the file doesn't exist already, so users can run
   240  	// their own mongodb server if they wish to.
   241  	if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) {
   242  		err = utils.AtomicWriteFile(
   243  			mongoConfigPath,
   244  			[]byte("ENABLE_MONGODB=no"),
   245  			0644,
   246  		)
   247  		if err != nil {
   248  			return err
   249  		}
   250  	}
   251  
   252  	if err := svc.Stop(); err != nil {
   253  		return errors.Annotatef(err, "failed to stop mongo")
   254  	}
   255  	if err := makeJournalDirs(dbDir); err != nil {
   256  		return fmt.Errorf("error creating journal directories: %v", err)
   257  	}
   258  	if err := preallocOplog(dbDir, oplogSizeMB); err != nil {
   259  		return fmt.Errorf("error creating oplog files: %v", err)
   260  	}
   261  	if err := service.InstallAndStart(svc); err != nil {
   262  		return errors.Trace(err)
   263  	}
   264  	return nil
   265  }
   266  
   267  // UpdateSSLKey writes a new SSL key used by mongo to validate connections from Juju state server(s)
   268  func UpdateSSLKey(dataDir, cert, privateKey string) error {
   269  	certKey := cert + "\n" + privateKey
   270  	err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600)
   271  	return errors.Annotate(err, "cannot write SSL key")
   272  }
   273  
   274  func makeJournalDirs(dataDir string) error {
   275  	journalDir := path.Join(dataDir, "journal")
   276  	if err := os.MkdirAll(journalDir, 0700); err != nil {
   277  		logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err)
   278  		return err
   279  	}
   280  
   281  	// Manually create the prealloc files, since otherwise they get
   282  	// created as 100M files. We create three files of 1MB each.
   283  	prefix := filepath.Join(journalDir, "prealloc.")
   284  	preallocSize := 1024 * 1024
   285  	return preallocFiles(prefix, preallocSize, preallocSize, preallocSize)
   286  }
   287  
   288  func logVersion(mongoPath string) {
   289  	cmd := exec.Command(mongoPath, "--version")
   290  	output, err := cmd.CombinedOutput()
   291  	if err != nil {
   292  		logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
   293  		return
   294  	}
   295  	logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
   296  }
   297  
   298  // getPackageManager is a helper function which returns the
   299  // package manager implementation for the current system.
   300  func getPackageManager() (manager.PackageManager, error) {
   301  	return manager.NewPackageManager(version.Current.Series)
   302  }
   303  
   304  // getPackagingConfigurer is a helper function which returns the
   305  // packaging configuration manager for the current system.
   306  func getPackagingConfigurer() (config.PackagingConfigurer, error) {
   307  	return config.NewPackagingConfigurer(version.Current.Series)
   308  }
   309  
   310  func installMongod(numaCtl bool) error {
   311  	series := version.Current.Series
   312  
   313  	pacconfer, err := getPackagingConfigurer()
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	pacman, err := getPackageManager()
   319  	if err != nil {
   320  		return err
   321  	}
   322  
   323  	// Only Quantal requires the PPA.
   324  	if series == "quantal" {
   325  		// install python-software-properties:
   326  		if err := pacman.InstallPrerequisite(); err != nil {
   327  			return err
   328  		}
   329  		if err := pacman.AddRepository("ppa:juju/stable"); err != nil {
   330  			return err
   331  		}
   332  	}
   333  	// CentOS requires "epel-release" for the epel repo mongodb-server is in.
   334  	if series == "centos7" {
   335  		// install epel-release
   336  		if err := pacman.Install("epel-release"); err != nil {
   337  			return err
   338  		}
   339  	}
   340  
   341  	mongoPkg := packageForSeries(series)
   342  
   343  	pkgs := []string{mongoPkg}
   344  	if numaCtl {
   345  		pkgs = []string{mongoPkg, numaCtlPkg}
   346  		logger.Infof("installing %s and %s", mongoPkg, numaCtlPkg)
   347  	} else {
   348  		logger.Infof("installing %s", mongoPkg)
   349  	}
   350  
   351  	for i, _ := range pkgs {
   352  		// apply release targeting if needed.
   353  		if pacconfer.IsCloudArchivePackage(pkgs[i]) {
   354  			pkgs[i] = strings.Join(pacconfer.ApplyCloudArchiveTarget(pkgs[i]), " ")
   355  		}
   356  
   357  		if err := pacman.Install(pkgs[i]); err != nil {
   358  			return err
   359  		}
   360  	}
   361  
   362  	// Work around SELinux on centos7
   363  	if series == "centos7" {
   364  		cmd := []string{"chcon", "-R", "-v", "-t", "mongod_var_lib_t", "/var/lib/juju/"}
   365  		logger.Infof("running %s %v", cmd[0], cmd[1:])
   366  		_, err = utils.RunCommand(cmd[0], cmd[1:]...)
   367  		if err != nil {
   368  			logger.Infof("chcon error %s", err)
   369  			logger.Infof("chcon error %s", err.Error())
   370  			return err
   371  		}
   372  
   373  		cmd = []string{"semanage", "port", "-a", "-t", "mongod_port_t", "-p", "tcp", "37017"}
   374  		logger.Infof("running %s %v", cmd[0], cmd[1:])
   375  		_, err = utils.RunCommand(cmd[0], cmd[1:]...)
   376  		if err != nil {
   377  			if !strings.Contains(err.Error(), "exit status 1") {
   378  				return err
   379  			}
   380  		}
   381  	}
   382  
   383  	return nil
   384  }
   385  
   386  // packageForSeries returns the name of the mongo package for the series
   387  // of the machine that it is going to be running on.
   388  func packageForSeries(series string) string {
   389  	switch series {
   390  	case "precise", "quantal", "raring", "saucy", "centos7":
   391  		return "mongodb-server"
   392  	default:
   393  		// trusty and onwards
   394  		return "juju-mongodb"
   395  	}
   396  }
   397  
   398  // noauthCommand returns an os/exec.Cmd that may be executed to
   399  // run mongod without security.
   400  func noauthCommand(dataDir string, port int) (*exec.Cmd, error) {
   401  	sslKeyFile := path.Join(dataDir, "server.pem")
   402  	dbDir := filepath.Join(dataDir, "db")
   403  	mongoPath, err := Path()
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  	cmd := exec.Command(mongoPath,
   408  		"--noauth",
   409  		"--dbpath", dbDir,
   410  		"--sslOnNormalPorts",
   411  		"--sslPEMKeyFile", sslKeyFile,
   412  		"--sslPEMKeyPassword", "ignored",
   413  		"--bind_ip", "127.0.0.1",
   414  		"--port", fmt.Sprint(port),
   415  		"--noprealloc",
   416  		"--syslog",
   417  		"--smallfiles",
   418  		"--journal",
   419  	)
   420  	return cmd, nil
   421  }