1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
27
"github.com/juju/juju/controller"
28
"github.com/juju/juju/network"
29
"github.com/juju/juju/service"
33
logger = loggo.GetLogger("juju.mongo")
34
mongoConfigPath = "/etc/default/mongodb"
36
// JujuMongod24Path holds the default path to the legacy Juju
38
JujuMongod24Path = "/usr/lib/juju/bin/mongod"
40
// This is NUMACTL package name for apt-get
41
numaCtlPkg = "numactl"
44
// StorageEngine represents the storage used by mongo.
45
type StorageEngine string
48
// JujuMongoPackage is the mongo package Juju uses when
50
JujuMongoPackage = "juju-mongodb3.2"
52
// JujuMongoTooldPackage is the mongo package Juju uses when
53
// installing mongo tools to get mongodump etc.
54
JujuMongoToolsPackage = "juju-mongo-tools3.2"
56
// MMAPV1 is the default storage engine in mongo db up to 3.x
57
MMAPV1 StorageEngine = "mmapv1"
59
// WiredTiger is a storage type introduced in 3
60
WiredTiger StorageEngine = "wiredTiger"
62
// Upgrading is a special case where mongo is being upgraded.
63
Upgrading StorageEngine = "Upgrading"
66
// Version represents the major.minor version of the runnig mongo.
70
Patch string // supports variants like 1-alpha
71
StorageEngine StorageEngine
74
// NewerThan will return 1 if the passed version is older than
75
// v, 0 if they are equal (or ver is a special case such as
76
// Upgrading and -1 if ver is newer.
77
func (v Version) NewerThan(ver Version) int {
78
if v == MongoUpgrade || ver == MongoUpgrade {
81
if v.Major > ver.Major {
84
if v.Major < ver.Major {
87
if v.Minor > ver.Minor {
90
if v.Minor < ver.Minor {
96
// NewVersion returns a mongo Version parsing the passed version string
97
// or error if not possible.
98
// A valid version string is of the form:
100
// major and minor are positive integers, patch is a string containing
101
// any ascii character except / and storage is one of the above defined
102
// StorageEngine. Only major is mandatory.
103
// An alternative valid string is 0.0/Upgrading which represents that
104
// mongo is being upgraded.
105
func NewVersion(v string) (Version, error) {
111
parts := strings.SplitN(v, "/", 2)
114
return Version{}, errors.New("invalid version string")
116
version.StorageEngine = MMAPV1
118
switch StorageEngine(parts[1]) {
120
version.StorageEngine = MMAPV1
122
version.StorageEngine = WiredTiger
124
version.StorageEngine = Upgrading
127
vParts := strings.SplitN(parts[0], ".", 3)
129
if len(vParts) >= 1 {
130
i, err := strconv.Atoi(vParts[0])
132
return Version{}, errors.Annotate(err, "Invalid version string, major is not an int")
136
if len(vParts) >= 2 {
137
i, err := strconv.Atoi(vParts[1])
139
return Version{}, errors.Annotate(err, "Invalid version string, minor is not an int")
143
if len(vParts) == 3 {
144
version.Patch = vParts[2]
147
if version.Major == 2 && version.StorageEngine == WiredTiger {
148
return Version{}, errors.Errorf("Version 2.x does not support Wired Tiger storage engine")
151
// This deserialises the special "Mongo Upgrading" version
152
if version.Major == 0 && version.Minor == 0 {
153
return Version{StorageEngine: Upgrading}, nil
159
// String serializes the version into a string.
160
func (v Version) String() string {
161
s := fmt.Sprintf("%d.%d", v.Major, v.Minor)
163
s = fmt.Sprintf("%s.%s", s, v.Patch)
165
if v.StorageEngine != "" {
166
s = fmt.Sprintf("%s/%s", s, v.StorageEngine)
171
// JujuMongodPath returns the path for the mongod binary
172
// with the specified version.
173
func JujuMongodPath(v Version) string {
174
return fmt.Sprintf("/usr/lib/juju/mongo%d.%d/bin/mongod", v.Major, v.Minor)
178
// Mongo24 represents juju-mongodb 2.4.x
179
Mongo24 = Version{Major: 2,
182
StorageEngine: MMAPV1,
184
// Mongo26 represents juju-mongodb26 2.6.x
185
Mongo26 = Version{Major: 2,
188
StorageEngine: MMAPV1,
190
// Mongo32wt represents juju-mongodb3 3.2.x with wiredTiger storage.
191
Mongo32wt = Version{Major: 3,
194
StorageEngine: WiredTiger,
196
// MongoUpgrade represents a sepacial case where an upgrade is in
198
MongoUpgrade = Version{Major: 0,
201
StorageEngine: Upgrading,
205
// InstalledVersion returns the version of mongo installed.
206
// We look for a specific, known version supported by this Juju,
207
// and fall back to the original mongo 2.4.
208
func InstalledVersion() Version {
209
mgoVersion := Mongo24
210
if binariesAvailable(Mongo32wt, os.Stat) {
211
mgoVersion = Mongo32wt
216
// binariesAvailable returns true if the binaries for the
217
// given Version of mongo are available.
218
func binariesAvailable(v Version, statFunc func(string) (os.FileInfo, error)) bool {
222
// 2.4 has a fixed path.
223
path = JujuMongod24Path
225
path = JujuMongodPath(v)
227
if _, err := statFunc(path); err == nil {
233
// WithAddresses represents an entity that has a set of
234
// addresses. e.g. a state Machine object
235
type WithAddresses interface {
236
Addresses() []network.Address
239
// IsMaster returns a boolean that represents whether the given
240
// machine's peer address is the primary mongo host for the replicaset
241
func IsMaster(session *mgo.Session, obj WithAddresses) (bool, error) {
242
addrs := obj.Addresses()
244
masterHostPort, err := replicaset.MasterHostPort(session)
246
// If the replica set has not been configured, then we
247
// can have only one master and the caller must
249
if err == replicaset.ErrMasterNotConfigured {
256
masterAddr, _, err := net.SplitHostPort(masterHostPort)
261
for _, addr := range addrs {
262
if addr.Value == masterAddr {
269
// SelectPeerAddress returns the address to use as the mongo replica set peer
270
// address by selecting it from the given addresses. If no addresses are
271
// available an empty string is returned.
272
func SelectPeerAddress(addrs []network.Address) string {
273
logger.Debugf("selecting mongo peer address from %+v", addrs)
274
// ScopeMachineLocal addresses are OK if we can't pick by space, also the
275
// second bool return is ignored intentionally.
276
addr, _ := network.SelectControllerAddress(addrs, true)
280
// SelectPeerHostPort returns the HostPort to use as the mongo replica set peer
281
// by selecting it from the given hostPorts.
282
func SelectPeerHostPort(hostPorts []network.HostPort) string {
283
logger.Debugf("selecting mongo peer hostPort by scope from %+v", hostPorts)
284
return network.SelectMongoHostPortsByScope(hostPorts, true)[0]
287
// SelectPeerHostPortBySpace returns the HostPort to use as the mongo replica set peer
288
// by selecting it from the given hostPorts.
289
func SelectPeerHostPortBySpace(hostPorts []network.HostPort, space network.SpaceName) string {
290
logger.Debugf("selecting mongo peer hostPort in space %s from %+v", space, hostPorts)
291
// ScopeMachineLocal addresses are OK if we can't pick by space.
292
suitableHostPorts, foundHostPortsInSpaces := network.SelectMongoHostPortsBySpaces(hostPorts, []network.SpaceName{space})
294
if !foundHostPortsInSpaces {
295
logger.Debugf("Failed to select hostPort by space - trying by scope from %+v", hostPorts)
296
suitableHostPorts = network.SelectMongoHostPortsByScope(hostPorts, true)
298
return suitableHostPorts[0]
301
// GenerateSharedSecret generates a pseudo-random shared secret (keyfile)
302
// for use with Mongo replica sets.
303
func GenerateSharedSecret() (string, error) {
304
// "A key’s length must be between 6 and 1024 characters and may
305
// only contain characters in the base64 set."
306
// -- http://docs.mongodb.org/manual/tutorial/generate-key-file/
307
buf := make([]byte, base64.StdEncoding.DecodedLen(1024))
308
if _, err := rand.Read(buf); err != nil {
309
return "", fmt.Errorf("cannot read random secret: %v", err)
311
return base64.StdEncoding.EncodeToString(buf), nil
314
// Path returns the executable path to be used to run mongod on this
315
// machine. If the juju-bundled version of mongo exists, it will return that
316
// path, otherwise it will return the command to run mongod from the path.
317
func Path(version Version) (string, error) {
318
return mongoPath(version, os.Stat, exec.LookPath)
321
func mongoPath(version Version, stat func(string) (os.FileInfo, error), lookPath func(string) (string, error)) (string, error) {
324
if _, err := stat(JujuMongod24Path); err == nil {
325
return JujuMongod24Path, nil
328
path, err := lookPath("mongod")
330
logger.Infof("could not find %v or mongod in $PATH", JujuMongod24Path)
335
path := JujuMongodPath(version)
337
if _, err = stat(path); err == nil {
342
logger.Infof("could not find a suitable binary for %q", version)
343
errMsg := fmt.Sprintf("no suitable binary for %q", version)
344
return "", errors.New(errMsg)
348
// EnsureServerParams is a parameter struct for EnsureServer.
349
type EnsureServerParams struct {
350
// APIPort is the port to connect to the api server.
353
// StatePort is the port to connect to the mongo server.
356
// Cert is the certificate.
359
// PrivateKey is the certificate's private key.
362
// CAPrivateKey is the CA certificate's private key.
365
// SharedSecret is a secret shared between mongo servers.
368
// SystemIdentity is the identity of the system.
369
SystemIdentity string
371
// DataDir is the machine agent data directory.
374
// Namespace is the machine agent's namespace, which is used to
375
// generate a unique service name for Mongo.
378
// OplogSize is the size of the Mongo oplog.
379
// If this is zero, then EnsureServer will
380
// calculate a default size according to the
381
// algorithm defined in Mongo.
384
// SetNumaControlPolicy preference - whether the user
385
// wants to set the numa control policy when starting mongo.
386
SetNumaControlPolicy bool
389
// EnsureServer ensures that the MongoDB server is installed,
390
// configured, and ready to run.
392
// This method will remove old versions of the mongo init service as necessary
393
// before installing the new version.
394
func EnsureServer(args EnsureServerParams) error {
396
"Ensuring mongo server is running; data directory %s; port %d",
397
args.DataDir, args.StatePort,
400
dbDir := filepath.Join(args.DataDir, "db")
401
if err := os.MkdirAll(dbDir, 0700); err != nil {
402
return fmt.Errorf("cannot create mongo database directory: %v", err)
405
oplogSizeMB := args.OplogSize
406
if oplogSizeMB == 0 {
408
if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
413
operatingsystem := series.HostSeries()
414
if err := installMongod(operatingsystem, args.SetNumaControlPolicy); err != nil {
415
// This isn't treated as fatal because the Juju MongoDB
416
// package is likely to be already installed anyway. There
417
// could just be a temporary issue with apt-get/yum/whatever
418
// and we don't want this to stop jujud from starting.
420
logger.Errorf("cannot install/upgrade mongod (will proceed anyway): %v", err)
422
mgoVersion := InstalledVersion()
423
mongoPath, err := Path(mgoVersion)
427
logVersion(mongoPath)
429
if err := UpdateSSLKey(args.DataDir, args.Cert, args.PrivateKey); err != nil {
433
err = utils.AtomicWriteFile(sharedSecretPath(args.DataDir), []byte(args.SharedSecret), 0600)
435
return fmt.Errorf("cannot write mongod shared secret: %v", err)
438
// Disable the default mongodb installed by the mongodb-server package.
439
// Only do this if the file doesn't exist already, so users can run
440
// their own mongodb server if they wish to.
441
if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) {
442
err = utils.AtomicWriteFile(
444
[]byte("ENABLE_MONGODB=no"),
452
svcConf := newConf(args.DataDir, dbDir, mongoPath, args.StatePort, oplogSizeMB, args.SetNumaControlPolicy, mgoVersion, true)
453
svc, err := newService(ServiceName, svcConf)
457
installed, err := svc.Installed()
459
return errors.Trace(err)
462
exists, err := svc.Exists()
464
return errors.Trace(err)
467
logger.Debugf("mongo exists as expected")
468
running, err := svc.Running()
470
return errors.Trace(err)
479
if err := svc.Stop(); err != nil {
480
return errors.Annotatef(err, "failed to stop mongo")
482
if err := makeJournalDirs(dbDir); err != nil {
483
return fmt.Errorf("error creating journal directories: %v", err)
485
if err := preallocOplog(dbDir, oplogSizeMB); err != nil {
486
return fmt.Errorf("error creating oplog files: %v", err)
488
if err := service.InstallAndStart(svc); err != nil {
489
return errors.Trace(err)
494
// UpdateSSLKey writes a new SSL key used by mongo to validate connections from Juju controller(s)
495
func UpdateSSLKey(dataDir, cert, privateKey string) error {
496
certKey := cert + "\n" + privateKey
497
err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600)
498
return errors.Annotate(err, "cannot write SSL key")
501
func makeJournalDirs(dataDir string) error {
502
journalDir := path.Join(dataDir, "journal")
503
if err := os.MkdirAll(journalDir, 0700); err != nil {
504
logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err)
508
// Manually create the prealloc files, since otherwise they get
509
// created as 100M files. We create three files of 1MB each.
510
prefix := filepath.Join(journalDir, "prealloc.")
511
preallocSize := 1024 * 1024
512
return preallocFiles(prefix, preallocSize, preallocSize, preallocSize)
515
func logVersion(mongoPath string) {
516
cmd := exec.Command(mongoPath, "--version")
517
output, err := cmd.CombinedOutput()
519
logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
522
logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
525
func installPackage(pkg string, pacconfer config.PackagingConfigurer, pacman manager.PackageManager) error {
526
// apply release targeting if needed.
527
if pacconfer.IsCloudArchivePackage(pkg) {
528
pkg = strings.Join(pacconfer.ApplyCloudArchiveTarget(pkg), " ")
531
return pacman.Install(pkg)
534
func installMongod(operatingsystem string, numaCtl bool) error {
535
// fetch the packaging configuration manager for the current operating system.
536
pacconfer, err := config.NewPackagingConfigurer(operatingsystem)
541
// fetch the package manager implementation for the current operating system.
542
pacman, err := manager.NewPackageManager(operatingsystem)
547
// CentOS requires "epel-release" for the epel repo mongodb-server is in.
548
if operatingsystem == "centos7" {
549
// install epel-release
550
if err := pacman.Install("epel-release"); err != nil {
555
mongoPkgs, fallbackPkgs := packagesForSeries(operatingsystem)
558
logger.Infof("installing %v and %s", mongoPkgs, numaCtlPkg)
559
if err = installPackage(numaCtlPkg, pacconfer, pacman); err != nil {
560
return errors.Trace(err)
563
logger.Infof("installing %v", mongoPkgs)
566
for i := range mongoPkgs {
567
if err = installPackage(mongoPkgs[i], pacconfer, pacman); err != nil {
571
if err != nil && len(fallbackPkgs) == 0 {
572
return errors.Trace(err)
575
logger.Errorf("installing mongo failed: %v", err)
576
logger.Infof("will try fallback packages %v", fallbackPkgs)
577
for i := range fallbackPkgs {
578
if err = installPackage(fallbackPkgs[i], pacconfer, pacman); err != nil {
579
return errors.Trace(err)
584
// Work around SELinux on centos7
585
if operatingsystem == "centos7" {
586
cmd := []string{"chcon", "-R", "-v", "-t", "mongod_var_lib_t", "/var/lib/juju/"}
587
logger.Infof("running %s %v", cmd[0], cmd[1:])
588
_, err = utils.RunCommand(cmd[0], cmd[1:]...)
590
logger.Errorf("chcon failed to change file security context error %s", err)
594
cmd = []string{"semanage", "port", "-a", "-t", "mongod_port_t", "-p", "tcp", strconv.Itoa(controller.DefaultStatePort)}
595
logger.Infof("running %s %v", cmd[0], cmd[1:])
596
_, err = utils.RunCommand(cmd[0], cmd[1:]...)
598
if !strings.Contains(err.Error(), "exit status 1") {
599
logger.Errorf("semanage failed to provide access on port %d error %s", controller.DefaultStatePort, err)
608
// packagesForSeries returns the name of the mongo package for the series
609
// of the machine that it is going to be running on plus a fallback for
610
// options where the package is going to be ready eventually but might not
612
func packagesForSeries(series string) ([]string, []string) {
614
case "precise", "quantal", "raring", "saucy", "centos7":
615
return []string{"mongodb-server"}, []string{}
616
case "trusty", "wily", "xenial":
617
return []string{JujuMongoPackage, JujuMongoToolsPackage}, []string{"juju-mongodb"}
620
return []string{JujuMongoPackage, JujuMongoToolsPackage}, []string{}
624
// DbDir returns the dir where mongo storage is.
625
func DbDir(dataDir string) string {
626
return filepath.Join(dataDir, "db")