~juju-qa/ubuntu/yakkety/juju/juju-1.25.8

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/mongo/mongo.go

  • Committer: Nicholas Skaggs
  • Date: 2016-12-02 17:28:37 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161202172837-jkrbdlyjcxtrii2n
Initial commit of 1.25.6

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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. If
 
80
// no addresses are available an empty string is returned.
 
81
func SelectPeerAddress(addrs []network.Address) string {
 
82
        addr, _ := network.SelectInternalAddress(addrs, true)
 
83
        return addr.Value
 
84
}
 
85
 
 
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)
 
90
}
 
91
 
 
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)
 
101
        }
 
102
        return base64.StdEncoding.EncodeToString(buf), nil
 
103
}
 
104
 
 
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
 
111
        }
 
112
 
 
113
        path, err := exec.LookPath("mongod")
 
114
        if err != nil {
 
115
                logger.Infof("could not find %v or mongod in $PATH", JujuMongodPath)
 
116
                return "", err
 
117
        }
 
118
        return path, nil
 
119
}
 
120
 
 
121
// EnsureServerParams is a parameter struct for EnsureServer.
 
122
type EnsureServerParams struct {
 
123
        // APIPort is the port to connect to the api server.
 
124
        APIPort int
 
125
 
 
126
        // StatePort is the port to connect to the mongo server.
 
127
        StatePort int
 
128
 
 
129
        // Cert is the certificate.
 
130
        Cert string
 
131
 
 
132
        // PrivateKey is the certificate's private key.
 
133
        PrivateKey string
 
134
 
 
135
        // CAPrivateKey is the CA certificate's private key.
 
136
        CAPrivateKey string
 
137
 
 
138
        // SharedSecret is a secret shared between mongo servers.
 
139
        SharedSecret string
 
140
 
 
141
        // SystemIdentity is the identity of the system.
 
142
        SystemIdentity string
 
143
 
 
144
        // DataDir is the machine agent data directory.
 
145
        DataDir string
 
146
 
 
147
        // Namespace is the machine agent's namespace, which is used to
 
148
        // generate a unique service name for Mongo.
 
149
        Namespace string
 
150
 
 
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.
 
155
        OplogSize int
 
156
 
 
157
        // SetNumaControlPolicy preference - whether the user
 
158
        // wants to set the numa control policy when starting mongo.
 
159
        SetNumaControlPolicy bool
 
160
}
 
161
 
 
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()
 
166
        if err != nil {
 
167
                return errors.Annotate(err, "cannot get mongo path")
 
168
        }
 
169
 
 
170
        dbDir := filepath.Join(dataDir, "db")
 
171
 
 
172
        if oplogSizeMB == 0 {
 
173
                var err error
 
174
                if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
 
175
                        return err
 
176
                }
 
177
        }
 
178
 
 
179
        svcConf := newConf(dataDir, dbDir, mongoPath, statePort, oplogSizeMB, setNumaControlPolicy)
 
180
        svc, err := newService(ServiceName(namespace), svcConf)
 
181
        if err != nil {
 
182
                return errors.Trace(err)
 
183
        }
 
184
        if err := svc.Remove(); err != nil {
 
185
                return errors.Trace(err)
 
186
        }
 
187
 
 
188
        if err := svc.Install(); err != nil {
 
189
                return errors.Trace(err)
 
190
        }
 
191
 
 
192
        return nil
 
193
}
 
194
 
 
195
// EnsureServer ensures that the MongoDB server is installed,
 
196
// configured, and ready to run.
 
197
//
 
198
// This method will remove old versions of the mongo init service as necessary
 
199
// before installing the new version.
 
200
//
 
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 {
 
205
        logger.Infof(
 
206
                "Ensuring mongo server is running; data directory %s; port %d",
 
207
                args.DataDir, args.StatePort,
 
208
        )
 
209
 
 
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)
 
213
        }
 
214
 
 
215
        oplogSizeMB := args.OplogSize
 
216
        if oplogSizeMB == 0 {
 
217
                var err error
 
218
                if oplogSizeMB, err = defaultOplogSize(dbDir); err != nil {
 
219
                        return err
 
220
                }
 
221
        }
 
222
 
 
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.
 
229
                // (LP #1441904)
 
230
                logger.Errorf("cannot install/upgrade mongod (will proceed anyway): %v", err)
 
231
        }
 
232
        mongoPath, err := Path()
 
233
        if err != nil {
 
234
                return err
 
235
        }
 
236
        logVersion(mongoPath)
 
237
 
 
238
        if err := UpdateSSLKey(args.DataDir, args.Cert, args.PrivateKey); err != nil {
 
239
                return err
 
240
        }
 
241
 
 
242
        err = utils.AtomicWriteFile(sharedSecretPath(args.DataDir), []byte(args.SharedSecret), 0600)
 
243
        if err != nil {
 
244
                return fmt.Errorf("cannot write mongod shared secret: %v", err)
 
245
        }
 
246
 
 
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(
 
252
                        mongoConfigPath,
 
253
                        []byte("ENABLE_MONGODB=no"),
 
254
                        0644,
 
255
                )
 
256
                if err != nil {
 
257
                        return err
 
258
                }
 
259
        }
 
260
 
 
261
        svcConf := newConf(args.DataDir, dbDir, mongoPath, args.StatePort, oplogSizeMB, args.SetNumaControlPolicy)
 
262
        svc, err := newService(ServiceName(args.Namespace), svcConf)
 
263
        if err != nil {
 
264
                return err
 
265
        }
 
266
        installed, err := svc.Installed()
 
267
        if err != nil {
 
268
                return errors.Trace(err)
 
269
        }
 
270
        if installed {
 
271
                exists, err := svc.Exists()
 
272
                if err != nil {
 
273
                        return errors.Trace(err)
 
274
                }
 
275
                if exists {
 
276
                        logger.Debugf("mongo exists as expected")
 
277
                        running, err := svc.Running()
 
278
                        if err != nil {
 
279
                                return errors.Trace(err)
 
280
                        }
 
281
                        if !running {
 
282
                                return svc.Start()
 
283
                        }
 
284
                        return nil
 
285
                }
 
286
        }
 
287
 
 
288
        if err := svc.Stop(); err != nil {
 
289
                return errors.Annotatef(err, "failed to stop mongo")
 
290
        }
 
291
        if err := makeJournalDirs(dbDir); err != nil {
 
292
                return fmt.Errorf("error creating journal directories: %v", err)
 
293
        }
 
294
        if err := preallocOplog(dbDir, oplogSizeMB); err != nil {
 
295
                return fmt.Errorf("error creating oplog files: %v", err)
 
296
        }
 
297
        if err := service.InstallAndStart(svc); err != nil {
 
298
                return errors.Trace(err)
 
299
        }
 
300
        return nil
 
301
}
 
302
 
 
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")
 
308
}
 
309
 
 
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)
 
314
                return err
 
315
        }
 
316
 
 
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)
 
322
}
 
323
 
 
324
func logVersion(mongoPath string) {
 
325
        cmd := exec.Command(mongoPath, "--version")
 
326
        output, err := cmd.CombinedOutput()
 
327
        if err != nil {
 
328
                logger.Infof("failed to read the output from %s --version: %v", mongoPath, err)
 
329
                return
 
330
        }
 
331
        logger.Debugf("using mongod: %s --version: %q", mongoPath, output)
 
332
}
 
333
 
 
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)
 
337
        if err != nil {
 
338
                return err
 
339
        }
 
340
 
 
341
        // fetch the package manager implementation for the current operating system.
 
342
        pacman, err := manager.NewPackageManager(operatingsystem)
 
343
        if err != nil {
 
344
                return err
 
345
        }
 
346
 
 
347
        // Only Quantal requires the PPA.
 
348
        if operatingsystem == "quantal" {
 
349
                // install python-software-properties:
 
350
                if err := pacman.InstallPrerequisite(); err != nil {
 
351
                        return err
 
352
                }
 
353
                if err := pacman.AddRepository("ppa:juju/stable"); err != nil {
 
354
                        return err
 
355
                }
 
356
        }
 
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 {
 
361
                        return err
 
362
                }
 
363
        }
 
364
 
 
365
        mongoPkg := packageForSeries(operatingsystem)
 
366
 
 
367
        pkgs := []string{mongoPkg}
 
368
        if numaCtl {
 
369
                pkgs = []string{mongoPkg, numaCtlPkg}
 
370
                logger.Infof("installing %s and %s", mongoPkg, numaCtlPkg)
 
371
        } else {
 
372
                logger.Infof("installing %s", mongoPkg)
 
373
        }
 
374
 
 
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]), " ")
 
379
                }
 
380
 
 
381
                if err := pacman.Install(pkgs[i]); err != nil {
 
382
                        return err
 
383
                }
 
384
        }
 
385
 
 
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:]...)
 
391
                if err != nil {
 
392
                        logger.Infof("chcon error %s", err)
 
393
                        logger.Infof("chcon error %s", err.Error())
 
394
                        return err
 
395
                }
 
396
 
 
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:]...)
 
400
                if err != nil {
 
401
                        if !strings.Contains(err.Error(), "exit status 1") {
 
402
                                return err
 
403
                        }
 
404
                }
 
405
        }
 
406
 
 
407
        return nil
 
408
}
 
409
 
 
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 {
 
413
        switch series {
 
414
        case "precise", "quantal", "raring", "saucy", "centos7":
 
415
                return "mongodb-server"
 
416
        default:
 
417
                // trusty and onwards
 
418
                return "juju-mongodb"
 
419
        }
 
420
}
 
421
 
 
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()
 
428
        if err != nil {
 
429
                return nil, err
 
430
        }
 
431
        cmd := exec.Command(mongoPath,
 
432
                "--noauth",
 
433
                "--dbpath", dbDir,
 
434
                "--sslOnNormalPorts",
 
435
                "--sslPEMKeyFile", sslKeyFile,
 
436
                "--sslPEMKeyPassword", "ignored",
 
437
                "--bind_ip", "127.0.0.1",
 
438
                "--port", fmt.Sprint(port),
 
439
                "--noprealloc",
 
440
                "--syslog",
 
441
                "--smallfiles",
 
442
                "--journal",
 
443
        )
 
444
        return cmd, nil
 
445
}