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 }