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