1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
25
"github.com/juju/juju/network"
26
"github.com/juju/juju/service"
27
"github.com/juju/juju/version"
31
logger = loggo.GetLogger("juju.mongo")
32
mongoConfigPath = "/etc/default/mongodb"
34
// JujuMongodPath holds the default path to the juju-specific
36
JujuMongodPath = "/usr/lib/juju/bin/mongod"
38
// This is NUMACTL package name for apt-get
39
numaCtlPkg = "numactl"
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
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()
53
masterHostPort, err := replicaset.MasterHostPort(session)
55
// If the replica set has not been configured, then we
56
// can have only one master and the caller must
58
if err == replicaset.ErrMasterNotConfigured {
65
masterAddr, _, err := net.SplitHostPort(masterHostPort)
70
for _, addr := range addrs {
71
if addr.Value == masterAddr {
78
// SelectPeerAddress returns the address to use as the
79
// mongo replica set peer address by selecting it from the given addresses. If
80
// no addresses are available an empty string is returned.
81
func SelectPeerAddress(addrs []network.Address) string {
82
addr, _ := network.SelectInternalAddress(addrs, true)
86
// SelectPeerHostPort returns the HostPort to use as the
87
// mongo replica set peer by selecting it from the given hostPorts.
88
func SelectPeerHostPort(hostPorts []network.HostPort) string {
89
return network.SelectInternalHostPort(hostPorts, true)
92
// GenerateSharedSecret generates a pseudo-random shared secret (keyfile)
93
// for use with Mongo replica sets.
94
func GenerateSharedSecret() (string, error) {
95
// "A key’s length must be between 6 and 1024 characters and may
96
// only contain characters in the base64 set."
97
// -- http://docs.mongodb.org/manual/tutorial/generate-key-file/
98
buf := make([]byte, base64.StdEncoding.DecodedLen(1024))
99
if _, err := rand.Read(buf); err != nil {
100
return "", fmt.Errorf("cannot read random secret: %v", err)
102
return base64.StdEncoding.EncodeToString(buf), nil
105
// Path returns the executable path to be used to run mongod on this
106
// machine. If the juju-bundled version of mongo exists, it will return that
107
// path, otherwise it will return the command to run mongod from the path.
108
func Path() (string, error) {
109
if _, err := os.Stat(JujuMongodPath); err == nil {
110
return JujuMongodPath, nil
113
path, err := exec.LookPath("mongod")
115
logger.Infof("could not find %v or mongod in $PATH", JujuMongodPath)
121
// EnsureServerParams is a parameter struct for EnsureServer.
122
type EnsureServerParams struct {
123
// APIPort is the port to connect to the api server.
126
// StatePort is the port to connect to the mongo server.
129
// Cert is the certificate.
132
// PrivateKey is the certificate's private key.
135
// CAPrivateKey is the CA certificate's private key.
138
// SharedSecret is a secret shared between mongo servers.
141
// SystemIdentity is the identity of the system.
142
SystemIdentity string
144
// DataDir is the machine agent data directory.
147
// Namespace is the machine agent's namespace, which is used to
148
// generate a unique service name for Mongo.
151
// OplogSize is the size of the Mongo oplog.
152
// If this is zero, then EnsureServer will
153
// calculate a default size according to the
154
// algorithm defined in Mongo.
157
// SetNumaControlPolicy preference - whether the user
158
// wants to set the numa control policy when starting mongo.
159
SetNumaControlPolicy bool
162
// EnsureServiceInstalled is a convenience method to [re]create
163
// the mongo service.
164
func EnsureServiceInstalled(dataDir, namespace string, statePort, oplogSizeMB int, setNumaControlPolicy bool) error {
165
mongoPath, err := Path()
167
return errors.Annotate(err, "cannot get mongo path")
170
dbDir := filepath.Join(dataDir, "db")
172
if oplogSizeMB == 0 {
174
if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
179
svcConf := newConf(dataDir, dbDir, mongoPath, statePort, oplogSizeMB, setNumaControlPolicy)
180
svc, err := newService(ServiceName(namespace), svcConf)
182
return errors.Trace(err)
184
if err := svc.Remove(); err != nil {
185
return errors.Trace(err)
188
if err := svc.Install(); err != nil {
189
return errors.Trace(err)
195
// EnsureServer ensures that the MongoDB server is installed,
196
// configured, and ready to run.
198
// This method will remove old versions of the mongo init service as necessary
199
// before installing the new version.
201
// The namespace is a unique identifier to prevent multiple instances of mongo
202
// on this machine from colliding. This should be empty unless using
203
// the local provider.
204
func EnsureServer(args EnsureServerParams) error {
206
"Ensuring mongo server is running; data directory %s; port %d",
207
args.DataDir, args.StatePort,
210
dbDir := filepath.Join(args.DataDir, "db")
211
if err := os.MkdirAll(dbDir, 0700); err != nil {
212
return fmt.Errorf("cannot create mongo database directory: %v", err)
215
oplogSizeMB := args.OplogSize
216
if oplogSizeMB == 0 {
218
if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
223
operatingsystem := version.Current.Series
224
if err := installMongod(operatingsystem, args.SetNumaControlPolicy); err != nil {
225
// This isn't treated as fatal because the Juju MongoDB
226
// package is likely to be already installed anyway. There
227
// could just be a temporary issue with apt-get/yum/whatever
228
// and we don't want this to stop jujud from starting.
230
logger.Errorf("cannot install/upgrade mongod (will proceed anyway): %v", err)
232
mongoPath, err := Path()
236
logVersion(mongoPath)
238
if err := UpdateSSLKey(args.DataDir, args.Cert, args.PrivateKey); err != nil {
242
err = utils.AtomicWriteFile(sharedSecretPath(args.DataDir), []byte(args.SharedSecret), 0600)
244
return fmt.Errorf("cannot write mongod shared secret: %v", err)
247
// Disable the default mongodb installed by the mongodb-server package.
248
// Only do this if the file doesn't exist already, so users can run
249
// their own mongodb server if they wish to.
250
if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) {
251
err = utils.AtomicWriteFile(
253
[]byte("ENABLE_MONGODB=no"),
261
svcConf := newConf(args.DataDir, dbDir, mongoPath, args.StatePort, oplogSizeMB, args.SetNumaControlPolicy)
262
svc, err := newService(ServiceName(args.Namespace), svcConf)
266
installed, err := svc.Installed()
268
return errors.Trace(err)
271
exists, err := svc.Exists()
273
return errors.Trace(err)
276
logger.Debugf("mongo exists as expected")
277
running, err := svc.Running()
279
return errors.Trace(err)
288
if err := svc.Stop(); err != nil {
289
return errors.Annotatef(err, "failed to stop mongo")
291
if err := makeJournalDirs(dbDir); err != nil {
292
return fmt.Errorf("error creating journal directories: %v", err)
294
if err := preallocOplog(dbDir, oplogSizeMB); err != nil {
295
return fmt.Errorf("error creating oplog files: %v", err)
297
if err := service.InstallAndStart(svc); err != nil {
298
return errors.Trace(err)
303
// UpdateSSLKey writes a new SSL key used by mongo to validate connections from Juju state server(s)
304
func UpdateSSLKey(dataDir, cert, privateKey string) error {
305
certKey := cert + "\n" + privateKey
306
err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600)
307
return errors.Annotate(err, "cannot write SSL key")
310
func makeJournalDirs(dataDir string) error {
311
journalDir := path.Join(dataDir, "journal")
312
if err := os.MkdirAll(journalDir, 0700); err != nil {
313
logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err)
317
// Manually create the prealloc files, since otherwise they get
318
// created as 100M files. We create three files of 1MB each.
319
prefix := filepath.Join(journalDir, "prealloc.")
320
preallocSize := 1024 * 1024
321
return preallocFiles(prefix, preallocSize, preallocSize, preallocSize)
324
func logVersion(mongoPath string) {
325
cmd := exec.Command(mongoPath, "--version")
326
output, err := cmd.CombinedOutput()
328
logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
331
logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
334
func installMongod(operatingsystem string, numaCtl bool) error {
335
// fetch the packaging configuration manager for the current operating system.
336
pacconfer, err := config.NewPackagingConfigurer(operatingsystem)
341
// fetch the package manager implementation for the current operating system.
342
pacman, err := manager.NewPackageManager(operatingsystem)
347
// Only Quantal requires the PPA.
348
if operatingsystem == "quantal" {
349
// install python-software-properties:
350
if err := pacman.InstallPrerequisite(); err != nil {
353
if err := pacman.AddRepository("ppa:juju/stable"); err != nil {
357
// CentOS requires "epel-release" for the epel repo mongodb-server is in.
358
if operatingsystem == "centos7" {
359
// install epel-release
360
if err := pacman.Install("epel-release"); err != nil {
365
mongoPkg := packageForSeries(operatingsystem)
367
pkgs := []string{mongoPkg}
369
pkgs = []string{mongoPkg, numaCtlPkg}
370
logger.Infof("installing %s and %s", mongoPkg, numaCtlPkg)
372
logger.Infof("installing %s", mongoPkg)
375
for i, _ := range pkgs {
376
// apply release targeting if needed.
377
if pacconfer.IsCloudArchivePackage(pkgs[i]) {
378
pkgs[i] = strings.Join(pacconfer.ApplyCloudArchiveTarget(pkgs[i]), " ")
381
if err := pacman.Install(pkgs[i]); err != nil {
386
// Work around SELinux on centos7
387
if operatingsystem == "centos7" {
388
cmd := []string{"chcon", "-R", "-v", "-t", "mongod_var_lib_t", "/var/lib/juju/"}
389
logger.Infof("running %s %v", cmd[0], cmd[1:])
390
_, err = utils.RunCommand(cmd[0], cmd[1:]...)
392
logger.Infof("chcon error %s", err)
393
logger.Infof("chcon error %s", err.Error())
397
cmd = []string{"semanage", "port", "-a", "-t", "mongod_port_t", "-p", "tcp", "37017"}
398
logger.Infof("running %s %v", cmd[0], cmd[1:])
399
_, err = utils.RunCommand(cmd[0], cmd[1:]...)
401
if !strings.Contains(err.Error(), "exit status 1") {
410
// packageForSeries returns the name of the mongo package for the series
411
// of the machine that it is going to be running on.
412
func packageForSeries(series string) string {
414
case "precise", "quantal", "raring", "saucy", "centos7":
415
return "mongodb-server"
417
// trusty and onwards
418
return "juju-mongodb"
422
// noauthCommand returns an os/exec.Cmd that may be executed to
423
// run mongod without security.
424
func noauthCommand(dataDir string, port int) (*exec.Cmd, error) {
425
sslKeyFile := path.Join(dataDir, "server.pem")
426
dbDir := filepath.Join(dataDir, "db")
427
mongoPath, err := Path()
431
cmd := exec.Command(mongoPath,
434
"--sslOnNormalPorts",
435
"--sslPEMKeyFile", sslKeyFile,
436
"--sslPEMKeyPassword", "ignored",
437
"--bind_ip", "127.0.0.1",
438
"--port", fmt.Sprint(port),