github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/agent/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 "bytes" 8 "crypto/rand" 9 "encoding/base64" 10 "fmt" 11 "net" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 17 "github.com/juju/loggo" 18 "github.com/juju/utils" 19 "github.com/juju/utils/apt" 20 "labix.org/v2/mgo" 21 22 "github.com/juju/juju/instance" 23 "github.com/juju/juju/replicaset" 24 "github.com/juju/juju/state/api/params" 25 "github.com/juju/juju/upstart" 26 "github.com/juju/juju/version" 27 ) 28 29 const ( 30 maxFiles = 65000 31 maxProcs = 20000 32 33 serviceName = "juju-db" 34 35 // SharedSecretFile is the name of the Mongo shared secret file 36 // located within the Juju data directory. 37 SharedSecretFile = "shared-secret" 38 39 // ReplicaSetName is the name of the replica set that juju uses for its 40 // state servers. 41 ReplicaSetName = "juju" 42 ) 43 44 var ( 45 logger = loggo.GetLogger("juju.agent.mongo") 46 mongoConfigPath = "/etc/default/mongodb" 47 48 // JujuMongodPath holds the default path to the juju-specific mongod. 49 JujuMongodPath = "/usr/lib/juju/bin/mongod" 50 51 upstartConfInstall = (*upstart.Conf).Install 52 upstartServiceStopAndRemove = (*upstart.Service).StopAndRemove 53 upstartServiceStop = (*upstart.Service).Stop 54 upstartServiceStart = (*upstart.Service).Start 55 ) 56 57 // WithAddresses represents an entity that has a set of 58 // addresses. e.g. a state Machine object 59 type WithAddresses interface { 60 Addresses() []instance.Address 61 } 62 63 // IsMaster returns a boolean that represents whether the given 64 // machine's peer address is the primary mongo host for the replicaset 65 func IsMaster(session *mgo.Session, obj WithAddresses) (bool, error) { 66 addrs := obj.Addresses() 67 68 masterHostPort, err := replicaset.MasterHostPort(session) 69 70 // If the replica set has not been configured, then we 71 // can have only one master and the caller must 72 // be that master. 73 if err == replicaset.ErrMasterNotConfigured { 74 return true, nil 75 } 76 77 if err != nil { 78 return false, err 79 } 80 81 masterAddr, _, err := net.SplitHostPort(masterHostPort) 82 if err != nil { 83 return false, err 84 } 85 86 machinePeerAddr := SelectPeerAddress(addrs) 87 return machinePeerAddr == masterAddr, nil 88 } 89 90 // SelectPeerAddress returns the address to use as the 91 // mongo replica set peer address by selecting it from the given addresses. 92 func SelectPeerAddress(addrs []instance.Address) string { 93 return instance.SelectInternalAddress(addrs, false) 94 } 95 96 // SelectPeerHostPort returns the HostPort to use as the 97 // mongo replica set peer by selecting it from the given hostPorts. 98 func SelectPeerHostPort(hostPorts []instance.HostPort) string { 99 return instance.SelectInternalHostPort(hostPorts, false) 100 } 101 102 // GenerateSharedSecret generates a pseudo-random shared secret (keyfile) 103 // for use with Mongo replica sets. 104 func GenerateSharedSecret() (string, error) { 105 // "A key’s length must be between 6 and 1024 characters and may 106 // only contain characters in the base64 set." 107 // -- http://docs.mongodb.org/manual/tutorial/generate-key-file/ 108 buf := make([]byte, base64.StdEncoding.DecodedLen(1024)) 109 if _, err := rand.Read(buf); err != nil { 110 return "", fmt.Errorf("cannot read random secret: %v", err) 111 } 112 return base64.StdEncoding.EncodeToString(buf), nil 113 } 114 115 // Path returns the executable path to be used to run mongod on this 116 // machine. If the juju-bundled version of mongo exists, it will return that 117 // path, otherwise it will return the command to run mongod from the path. 118 func Path() (string, error) { 119 if _, err := os.Stat(JujuMongodPath); err == nil { 120 return JujuMongodPath, nil 121 } 122 123 path, err := exec.LookPath("mongod") 124 if err != nil { 125 logger.Infof("could not find %v or mongod in $PATH", JujuMongodPath) 126 return "", err 127 } 128 return path, nil 129 } 130 131 // RemoveService removes the mongoDB upstart service from this machine. 132 func RemoveService(namespace string) error { 133 svc := upstart.NewService(ServiceName(namespace)) 134 return upstartServiceStopAndRemove(svc) 135 } 136 137 const ( 138 // WithHA is used when we want to start a mongo service with HA support. 139 WithHA = true 140 // WithoutHA is used when we want to start a mongo service without HA support. 141 WithoutHA = false 142 ) 143 144 // EnsureMongoServer ensures that the correct mongo upstart script is installed 145 // and running. 146 // 147 // This method will remove old versions of the mongo upstart script as necessary 148 // before installing the new version. 149 // 150 // The namespace is a unique identifier to prevent multiple instances of mongo 151 // on this machine from colliding. This should be empty unless using 152 // the local provider. 153 func EnsureServer(dataDir string, namespace string, info params.StateServingInfo, withHA bool) error { 154 logger.Infof("Ensuring mongo server is running; data directory %s; port %d", dataDir, info.StatePort) 155 dbDir := filepath.Join(dataDir, "db") 156 157 if err := os.MkdirAll(dbDir, 0700); err != nil { 158 return fmt.Errorf("cannot create mongo database directory: %v", err) 159 } 160 161 certKey := info.Cert + "\n" + info.PrivateKey 162 err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600) 163 if err != nil { 164 return fmt.Errorf("cannot write SSL key: %v", err) 165 } 166 167 err = utils.AtomicWriteFile(sharedSecretPath(dataDir), []byte(info.SharedSecret), 0600) 168 if err != nil { 169 return fmt.Errorf("cannot write mongod shared secret: %v", err) 170 } 171 172 // Disable the default mongodb installed by the mongodb-server package. 173 // Only do this if the file doesn't exist already, so users can run 174 // their own mongodb server if they wish to. 175 if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) { 176 err = utils.AtomicWriteFile( 177 mongoConfigPath, 178 []byte("ENABLE_MONGODB=no"), 179 0644, 180 ) 181 if err != nil { 182 return err 183 } 184 } 185 186 if err := aptGetInstallMongod(); err != nil { 187 return fmt.Errorf("cannot install mongod: %v", err) 188 } 189 190 upstartConf, mongoPath, err := upstartService(namespace, dataDir, dbDir, info.StatePort, withHA) 191 if err != nil { 192 return err 193 } 194 logVersion(mongoPath) 195 196 if err := upstartServiceStop(&upstartConf.Service); err != nil { 197 return fmt.Errorf("failed to stop mongo: %v", err) 198 } 199 if err := makeJournalDirs(dbDir); err != nil { 200 return fmt.Errorf("error creating journal directories: %v", err) 201 } 202 return upstartConfInstall(upstartConf) 203 } 204 205 // ServiceName returns the name of the upstart service config for mongo using 206 // the given namespace. 207 func ServiceName(namespace string) string { 208 if namespace != "" { 209 return fmt.Sprintf("%s-%s", serviceName, namespace) 210 } 211 return serviceName 212 } 213 214 func makeJournalDirs(dataDir string) error { 215 journalDir := path.Join(dataDir, "journal") 216 217 if err := os.MkdirAll(journalDir, 0700); err != nil { 218 logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err) 219 return err 220 } 221 222 // manually create the prealloc files, since otherwise they get created as 100M files. 223 zeroes := make([]byte, 64*1024) // should be enough for anyone 224 for x := 0; x < 3; x++ { 225 name := fmt.Sprintf("prealloc.%d", x) 226 filename := filepath.Join(journalDir, name) 227 f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) 228 // TODO(jam) 2014-04-12 https://launchpad.net/bugs/1306902 229 // When we support upgrading Mongo into Replica mode, we should 230 // start rewriting the upstart config 231 if os.IsExist(err) { 232 // already exists, don't overwrite 233 continue 234 } 235 if err != nil { 236 return fmt.Errorf("failed to open mongo prealloc file %q: %v", filename, err) 237 } 238 defer f.Close() 239 for total := 0; total < 1024*1024; { 240 n, err := f.Write(zeroes) 241 if err != nil { 242 return fmt.Errorf("failed to write to mongo prealloc file %q: %v", filename, err) 243 } 244 total += n 245 } 246 } 247 return nil 248 } 249 250 func logVersion(mongoPath string) { 251 cmd := exec.Command(mongoPath, "--version") 252 output, err := cmd.CombinedOutput() 253 if err != nil { 254 logger.Infof("failed to read the output from %s --version: %v", mongoPath, err) 255 return 256 } 257 logger.Debugf("using mongod: %s --version: %q", mongoPath, output) 258 } 259 260 func sslKeyPath(dataDir string) string { 261 return filepath.Join(dataDir, "server.pem") 262 } 263 264 func sharedSecretPath(dataDir string) string { 265 return filepath.Join(dataDir, SharedSecretFile) 266 } 267 268 // upstartService returns the upstart config for the mongo state service. 269 // It also returns the path to the mongod executable that the upstart config 270 // will be using. 271 func upstartService(namespace, dataDir, dbDir string, port int, withHA bool) (*upstart.Conf, string, error) { 272 svc := upstart.NewService(ServiceName(namespace)) 273 274 mongoPath, err := Path() 275 if err != nil { 276 return nil, "", err 277 } 278 279 mongoCmd := mongoPath + " --auth" + 280 " --dbpath=" + utils.ShQuote(dbDir) + 281 " --sslOnNormalPorts" + 282 " --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(dataDir)) + 283 " --sslPEMKeyPassword ignored" + 284 " --bind_ip 0.0.0.0" + 285 " --port " + fmt.Sprint(port) + 286 " --noprealloc" + 287 " --syslog" + 288 " --smallfiles" + 289 " --journal" + 290 " --keyFile " + utils.ShQuote(sharedSecretPath(dataDir)) 291 if withHA { 292 mongoCmd += " --replSet " + ReplicaSetName 293 } 294 conf := &upstart.Conf{ 295 Service: *svc, 296 Desc: "juju state database", 297 Limit: map[string]string{ 298 "nofile": fmt.Sprintf("%d %d", maxFiles, maxFiles), 299 "nproc": fmt.Sprintf("%d %d", maxProcs, maxProcs), 300 }, 301 Cmd: mongoCmd, 302 } 303 return conf, mongoPath, nil 304 } 305 306 func aptGetInstallMongod() error { 307 // Only Quantal requires the PPA. 308 if version.Current.Series == "quantal" { 309 if err := addAptRepository("ppa:juju/stable"); err != nil { 310 return err 311 } 312 } 313 pkg := packageForSeries(version.Current.Series) 314 cmds := apt.GetPreparePackages([]string{pkg}, version.Current.Series) 315 logger.Infof("installing %s", pkg) 316 for _, cmd := range cmds { 317 if err := apt.GetInstall(cmd...); err != nil { 318 return err 319 } 320 } 321 return nil 322 } 323 324 func addAptRepository(name string) error { 325 // add-apt-repository requires python-software-properties 326 cmds := apt.GetPreparePackages( 327 []string{"python-software-properties"}, 328 version.Current.Series, 329 ) 330 logger.Infof("installing python-software-properties") 331 for _, cmd := range cmds { 332 if err := apt.GetInstall(cmd...); err != nil { 333 return err 334 } 335 } 336 337 logger.Infof("adding apt repository %q", name) 338 cmd := exec.Command("add-apt-repository", "-y", name) 339 out, err := cmd.CombinedOutput() 340 if err != nil { 341 return fmt.Errorf("cannot add apt repository: %v (output %s)", err, bytes.TrimSpace(out)) 342 } 343 return nil 344 } 345 346 // packageForSeries returns the name of the mongo package for the series 347 // of the machine that it is going to be running on. 348 func packageForSeries(series string) string { 349 switch series { 350 case "precise", "quantal", "raring", "saucy": 351 return "mongodb-server" 352 default: 353 // trusty and onwards 354 return "juju-mongodb" 355 } 356 } 357 358 // noauthCommand returns an os/exec.Cmd that may be executed to 359 // run mongod without security. 360 func noauthCommand(dataDir string, port int) (*exec.Cmd, error) { 361 sslKeyFile := path.Join(dataDir, "server.pem") 362 dbDir := filepath.Join(dataDir, "db") 363 mongoPath, err := Path() 364 if err != nil { 365 return nil, err 366 } 367 cmd := exec.Command(mongoPath, 368 "--noauth", 369 "--dbpath", dbDir, 370 "--sslOnNormalPorts", 371 "--sslPEMKeyFile", sslKeyFile, 372 "--sslPEMKeyPassword", "ignored", 373 "--bind_ip", "127.0.0.1", 374 "--port", fmt.Sprint(port), 375 "--noprealloc", 376 "--syslog", 377 "--smallfiles", 378 "--journal", 379 ) 380 return cmd, nil 381 }