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