github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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/network" 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.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() []network.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 []network.Address) string { 93 return network.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 []network.HostPort) string { 99 return network.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 // EnsureMongoServer ensures that the correct mongo upstart script is installed 138 // and running. 139 // 140 // This method will remove old versions of the mongo upstart script as necessary 141 // before installing the new version. 142 // 143 // The namespace is a unique identifier to prevent multiple instances of mongo 144 // on this machine from colliding. This should be empty unless using 145 // the local provider. 146 func EnsureServer(dataDir string, namespace string, info params.StateServingInfo) error { 147 logger.Infof("Ensuring mongo server is running; data directory %s; port %d", dataDir, info.StatePort) 148 dbDir := filepath.Join(dataDir, "db") 149 150 if err := os.MkdirAll(dbDir, 0700); err != nil { 151 return fmt.Errorf("cannot create mongo database directory: %v", err) 152 } 153 154 certKey := info.Cert + "\n" + info.PrivateKey 155 err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(certKey), 0600) 156 if err != nil { 157 return fmt.Errorf("cannot write SSL key: %v", err) 158 } 159 160 err = utils.AtomicWriteFile(sharedSecretPath(dataDir), []byte(info.SharedSecret), 0600) 161 if err != nil { 162 return fmt.Errorf("cannot write mongod shared secret: %v", err) 163 } 164 165 // Disable the default mongodb installed by the mongodb-server package. 166 // Only do this if the file doesn't exist already, so users can run 167 // their own mongodb server if they wish to. 168 if _, err := os.Stat(mongoConfigPath); os.IsNotExist(err) { 169 err = utils.AtomicWriteFile( 170 mongoConfigPath, 171 []byte("ENABLE_MONGODB=no"), 172 0644, 173 ) 174 if err != nil { 175 return err 176 } 177 } 178 179 if err := aptGetInstallMongod(); err != nil { 180 return fmt.Errorf("cannot install mongod: %v", err) 181 } 182 183 upstartConf, mongoPath, err := upstartService(namespace, dataDir, dbDir, info.StatePort) 184 if err != nil { 185 return err 186 } 187 logVersion(mongoPath) 188 189 if err := upstartServiceStop(&upstartConf.Service); err != nil { 190 return fmt.Errorf("failed to stop mongo: %v", err) 191 } 192 if err := makeJournalDirs(dbDir); err != nil { 193 return fmt.Errorf("error creating journal directories: %v", err) 194 } 195 if err := preallocOplog(dbDir); err != nil { 196 return fmt.Errorf("error creating oplog files: %v", err) 197 } 198 return upstartConfInstall(upstartConf) 199 } 200 201 // ServiceName returns the name of the upstart service config for mongo using 202 // the given namespace. 203 func ServiceName(namespace string) string { 204 if namespace != "" { 205 return fmt.Sprintf("%s-%s", serviceName, namespace) 206 } 207 return serviceName 208 } 209 210 func makeJournalDirs(dataDir string) error { 211 journalDir := path.Join(dataDir, "journal") 212 if err := os.MkdirAll(journalDir, 0700); err != nil { 213 logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err) 214 return err 215 } 216 217 // Manually create the prealloc files, since otherwise they get 218 // created as 100M files. We create three files of 1MB each. 219 prefix := filepath.Join(journalDir, "prealloc.") 220 preallocSize := 1024 * 1024 221 return preallocFiles(prefix, preallocSize, preallocSize, preallocSize) 222 } 223 224 func logVersion(mongoPath string) { 225 cmd := exec.Command(mongoPath, "--version") 226 output, err := cmd.CombinedOutput() 227 if err != nil { 228 logger.Infof("failed to read the output from %s --version: %v", mongoPath, err) 229 return 230 } 231 logger.Debugf("using mongod: %s --version: %q", mongoPath, output) 232 } 233 234 func sslKeyPath(dataDir string) string { 235 return filepath.Join(dataDir, "server.pem") 236 } 237 238 func sharedSecretPath(dataDir string) string { 239 return filepath.Join(dataDir, SharedSecretFile) 240 } 241 242 // upstartService returns the upstart config for the mongo state service. 243 // It also returns the path to the mongod executable that the upstart config 244 // will be using. 245 func upstartService(namespace, dataDir, dbDir string, port int) (*upstart.Conf, string, error) { 246 svc := upstart.NewService(ServiceName(namespace)) 247 248 mongoPath, err := Path() 249 if err != nil { 250 return nil, "", err 251 } 252 253 mongoCmd := mongoPath + " --auth" + 254 " --dbpath=" + utils.ShQuote(dbDir) + 255 " --sslOnNormalPorts" + 256 " --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(dataDir)) + 257 " --sslPEMKeyPassword ignored" + 258 " --bind_ip 0.0.0.0" + 259 " --port " + fmt.Sprint(port) + 260 " --noprealloc" + 261 " --syslog" + 262 " --smallfiles" + 263 " --journal" + 264 " --keyFile " + utils.ShQuote(sharedSecretPath(dataDir)) + 265 " --replSet " + ReplicaSetName 266 conf := &upstart.Conf{ 267 Service: *svc, 268 Desc: "juju state database", 269 Limit: map[string]string{ 270 "nofile": fmt.Sprintf("%d %d", maxFiles, maxFiles), 271 "nproc": fmt.Sprintf("%d %d", maxProcs, maxProcs), 272 }, 273 Cmd: mongoCmd, 274 } 275 return conf, mongoPath, nil 276 } 277 278 func aptGetInstallMongod() error { 279 // Only Quantal requires the PPA. 280 if version.Current.Series == "quantal" { 281 if err := addAptRepository("ppa:juju/stable"); err != nil { 282 return err 283 } 284 } 285 pkg := packageForSeries(version.Current.Series) 286 cmds := apt.GetPreparePackages([]string{pkg}, version.Current.Series) 287 logger.Infof("installing %s", pkg) 288 for _, cmd := range cmds { 289 if err := apt.GetInstall(cmd...); err != nil { 290 return err 291 } 292 } 293 return nil 294 } 295 296 func addAptRepository(name string) error { 297 // add-apt-repository requires python-software-properties 298 cmds := apt.GetPreparePackages( 299 []string{"python-software-properties"}, 300 version.Current.Series, 301 ) 302 logger.Infof("installing python-software-properties") 303 for _, cmd := range cmds { 304 if err := apt.GetInstall(cmd...); err != nil { 305 return err 306 } 307 } 308 309 logger.Infof("adding apt repository %q", name) 310 cmd := exec.Command("add-apt-repository", "-y", name) 311 out, err := cmd.CombinedOutput() 312 if err != nil { 313 return fmt.Errorf("cannot add apt repository: %v (output %s)", err, bytes.TrimSpace(out)) 314 } 315 return nil 316 } 317 318 // packageForSeries returns the name of the mongo package for the series 319 // of the machine that it is going to be running on. 320 func packageForSeries(series string) string { 321 switch series { 322 case "precise", "quantal", "raring", "saucy": 323 return "mongodb-server" 324 default: 325 // trusty and onwards 326 return "juju-mongodb" 327 } 328 } 329 330 // noauthCommand returns an os/exec.Cmd that may be executed to 331 // run mongod without security. 332 func noauthCommand(dataDir string, port int) (*exec.Cmd, error) { 333 sslKeyFile := path.Join(dataDir, "server.pem") 334 dbDir := filepath.Join(dataDir, "db") 335 mongoPath, err := Path() 336 if err != nil { 337 return nil, err 338 } 339 cmd := exec.Command(mongoPath, 340 "--noauth", 341 "--dbpath", dbDir, 342 "--sslOnNormalPorts", 343 "--sslPEMKeyFile", sslKeyFile, 344 "--sslPEMKeyPassword", "ignored", 345 "--bind_ip", "127.0.0.1", 346 "--port", fmt.Sprint(port), 347 "--noprealloc", 348 "--syslog", 349 "--smallfiles", 350 "--journal", 351 ) 352 return cmd, nil 353 }