github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 "context" 8 "crypto/rand" 9 "encoding/base64" 10 "fmt" 11 "net" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 "regexp" 17 "time" 18 19 "github.com/juju/clock" 20 "github.com/juju/errors" 21 "github.com/juju/loggo" 22 "github.com/juju/mgo/v3" 23 "github.com/juju/replicaset/v3" 24 "github.com/juju/retry" 25 "github.com/juju/utils/v3" 26 27 "github.com/juju/juju/core/base" 28 "github.com/juju/juju/core/network" 29 coreos "github.com/juju/juju/core/os" 30 "github.com/juju/juju/packaging" 31 "github.com/juju/juju/packaging/dependency" 32 "github.com/juju/juju/service/common" 33 "github.com/juju/juju/service/snap" 34 "github.com/juju/juju/service/systemd" 35 ) 36 37 var logger = loggo.GetLogger("juju.mongo") 38 39 // StorageEngine represents the storage used by mongo. 40 type StorageEngine string 41 42 const ( 43 // JujuDbSnap is the snap of MongoDB that Juju uses. 44 JujuDbSnap = "juju-db" 45 46 // WiredTiger is a storage type introduced in 3 47 WiredTiger StorageEngine = "wiredTiger" 48 ) 49 50 // JujuDbSnapMongodPath is the path that the juju-db snap 51 // makes mongod available at 52 var JujuDbSnapMongodPath = "/snap/bin/juju-db.mongod" 53 54 // WithAddresses represents an entity that has a set of 55 // addresses. e.g. a state Machine object 56 type WithAddresses interface { 57 Addresses() network.SpaceAddresses 58 } 59 60 // IsMaster returns a boolean that represents whether the given 61 // machine's peer address is the primary mongo host for the replicaset 62 var IsMaster = isMaster 63 64 func isMaster(session *mgo.Session, obj WithAddresses) (bool, error) { 65 addrs := obj.Addresses() 66 67 masterHostPort, err := replicaset.MasterHostPort(session) 68 69 // If the replica set has not been configured, then we 70 // can have only one master and the caller must 71 // be that master. 72 if err == replicaset.ErrMasterNotConfigured { 73 return true, nil 74 } 75 if err != nil { 76 return false, err 77 } 78 79 masterAddr, _, err := net.SplitHostPort(masterHostPort) 80 if err != nil { 81 return false, err 82 } 83 84 for _, addr := range addrs { 85 if addr.Value == masterAddr { 86 return true, nil 87 } 88 } 89 return false, nil 90 } 91 92 // SelectPeerAddress returns the address to use as the mongo replica set peer 93 // address by selecting it from the given addresses. 94 // If no addresses are available an empty string is returned. 95 func SelectPeerAddress(addrs network.ProviderAddresses) string { 96 // The second bool result is ignored intentionally (we return an empty 97 // string if no suitable address is available.) 98 addr, _ := addrs.OneMatchingScope(network.ScopeMatchCloudLocal) 99 return addr.Value 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 /* 116 Values set as per bug: 117 https://bugs.launchpad.net/juju/+bug/1656430 118 net.ipv4.tcp_max_syn_backlog = 4096 119 net.core.somaxconn = 16384 120 net.core.netdev_max_backlog = 1000 121 net.ipv4.tcp_fin_timeout = 30 122 123 Values set as per mongod recommendation (see syslog on default mongod run) 124 /sys/kernel/mm/transparent_hugepage/enabled 'always' > 'never' 125 /sys/kernel/mm/transparent_hugepage/defrag 'always' > 'never' 126 */ 127 // TODO(bootstrap): tweaks this to mongo OCI image. 128 var mongoKernelTweaks = map[string]string{ 129 "/sys/kernel/mm/transparent_hugepage/enabled": "never", 130 "/sys/kernel/mm/transparent_hugepage/defrag": "never", 131 "/proc/sys/net/ipv4/tcp_max_syn_backlog": "4096", 132 "/proc/sys/net/core/somaxconn": "16384", 133 "/proc/sys/net/core/netdev_max_backlog": "1000", 134 "/proc/sys/net/ipv4/tcp_fin_timeout": "30", 135 } 136 137 // NewMemoryProfile returns a Memory Profile from the passed value. 138 func NewMemoryProfile(m string) (MemoryProfile, error) { 139 mp := MemoryProfile(m) 140 if err := mp.Validate(); err != nil { 141 return MemoryProfile(""), err 142 } 143 return mp, nil 144 } 145 146 // MemoryProfile represents a type of meory configuration for Mongo. 147 type MemoryProfile string 148 149 // String returns a string representation of this profile value. 150 func (m MemoryProfile) String() string { 151 return string(m) 152 } 153 154 func (m MemoryProfile) Validate() error { 155 if m != MemoryProfileLow && m != MemoryProfileDefault { 156 return errors.NotValidf("memory profile %q", m) 157 } 158 return nil 159 } 160 161 const ( 162 // MemoryProfileLow will use as little memory as possible in mongo. 163 MemoryProfileLow MemoryProfile = "low" 164 // MemoryProfileDefault will use mongo config ootb. 165 MemoryProfileDefault MemoryProfile = "default" 166 ) 167 168 // EnsureServerParams is a parameter struct for EnsureServer. 169 type EnsureServerParams struct { 170 // APIPort is the port to connect to the api server. 171 APIPort int 172 173 // StatePort is the port to connect to the mongo server. 174 StatePort int 175 176 // Cert is the certificate. 177 Cert string 178 179 // PrivateKey is the certificate's private key. 180 PrivateKey string 181 182 // CAPrivateKey is the CA certificate's private key. 183 CAPrivateKey string 184 185 // SharedSecret is a secret shared between mongo servers. 186 SharedSecret string 187 188 // SystemIdentity is the identity of the system. 189 SystemIdentity string 190 191 // DataDir is the machine agent data directory. 192 DataDir string 193 194 // ConfigDir is where mongo config goes. 195 ConfigDir string 196 197 // Namespace is the machine agent's namespace, which is used to 198 // generate a unique service name for Mongo. 199 Namespace string 200 201 // OplogSize is the size of the Mongo oplog. 202 // If this is zero, then EnsureServer will 203 // calculate a default size according to the 204 // algorithm defined in Mongo. 205 OplogSize int 206 207 // SetNUMAControlPolicy preference - whether the user 208 // wants to set the numa control policy when starting mongo. 209 SetNUMAControlPolicy bool 210 211 // MemoryProfile determines which value is going to be used by 212 // the cache and future memory tweaks. 213 MemoryProfile MemoryProfile 214 215 // The channel for installing the mongo snap in focal and later. 216 JujuDBSnapChannel string 217 } 218 219 // EnsureServerInstalled ensures that the MongoDB server is installed, 220 // configured, and ready to run. 221 func EnsureServerInstalled(ctx context.Context, args EnsureServerParams) error { 222 return ensureServer(ctx, args, mongoKernelTweaks) 223 } 224 225 func ensureServer(ctx context.Context, args EnsureServerParams, mongoKernelTweaks map[string]string) (err error) { 226 tweakSysctlForMongo(mongoKernelTweaks) 227 228 mongoDep := dependency.Mongo(args.JujuDBSnapChannel) 229 if args.DataDir == "" { 230 args.DataDir = dataPathForJujuDbSnap 231 } 232 if args.ConfigDir == "" { 233 args.ConfigDir = systemd.EtcSystemdDir 234 } 235 236 logger.Infof( 237 "Ensuring mongo server is running; data directory %s; port %d", 238 args.DataDir, args.StatePort, 239 ) 240 241 if err := setupDataDirectory(args); err != nil { 242 return errors.Annotatef(err, "cannot set up data directory") 243 } 244 245 // TODO(wallyworld) - set up Numactl if requested in args.SetNUMAControlPolicy 246 svc, err := mongoSnapService(args.DataDir, args.ConfigDir, args.JujuDBSnapChannel) 247 if err != nil { 248 return errors.Annotatef(err, "cannot create mongo snap service") 249 } 250 251 hostBase, err := coreos.HostBase() 252 if err != nil { 253 return errors.Annotatef(err, "cannot get host base") 254 } 255 256 if err := installMongod(mongoDep, hostBase, svc); err != nil { 257 return errors.Annotatef(err, "cannot install mongod") 258 } 259 260 finder := NewMongodFinder() 261 mongoPath, err := finder.InstalledAt() 262 if err != nil { 263 return errors.Annotatef(err, "unable to find mongod install path") 264 } 265 logVersion(mongoPath) 266 267 oplogSizeMB := args.OplogSize 268 if oplogSizeMB == 0 { 269 oplogSizeMB, err = defaultOplogSize(dbDir(args.DataDir)) 270 if err != nil { 271 return errors.Annotatef(err, "unable to calculate default oplog size") 272 } 273 } 274 275 mongoArgs := generateConfig(oplogSizeMB, args) 276 277 // Update snap configuration. 278 // TODO(tsm): refactor out to service.Configure 279 err = mongoArgs.writeConfig(configPath(args.DataDir)) 280 if err != nil { 281 return errors.Annotatef(err, "unable to write config") 282 } 283 if err := snap.SetSnapConfig(ServiceName, "configpath", configPath(args.DataDir)); err != nil { 284 return errors.Annotatef(err, "unable to set snap config") 285 } 286 287 // Update the systemd service configuration. 288 if err := svc.ConfigOverride(); err != nil { 289 return errors.Annotatef(err, "unable to update systemd service configuration") 290 } 291 292 // Ensure the mongo service is running, after we've installed and 293 // configured it. 294 // We do this in two retry loops. The outer loop, will try and start 295 // the service repeatedly over the span of 5 minutes. The inner loop will 296 // try and ensure that the service is running over the span of 10 seconds. 297 // If the service is running, then it will return nil, causing the outer 298 // loop to complete. If the service is not running, and the inner retry loop 299 // has been exhausted, then the outer loop will attempt to start the service 300 // again after a delay. 301 // If the mongo service is not installed, then nothing we do here, will 302 // cause the service to start. So we will just return the error. 303 return retry.Call(retry.CallArgs{ 304 Func: func() error { 305 if err := svc.Start(); err != nil { 306 logger.Debugf("cannot start mongo service: %v", err) 307 } 308 return ensureMongoServiceRunning(ctx, svc) 309 }, 310 IsFatalError: func(err error) bool { 311 // If the service is not installed, then we should attempt 312 // to install it again, by bouncing. 313 return errors.Cause(err) == ErrMongoServiceNotInstalled 314 }, 315 NotifyFunc: func(err error, attempt int) { 316 logger.Debugf("attempt %d to start mongo service: %v", attempt, err) 317 }, 318 Stop: ctx.Done(), 319 Attempts: -1, 320 Delay: 10 * time.Second, 321 MaxDelay: 1 * time.Minute, 322 MaxDuration: time.Minute * 5, 323 BackoffFunc: retry.DoubleDelay, 324 Clock: clock.WallClock, 325 }) 326 } 327 328 const ( 329 // ErrMongoServiceNotInstalled is returned when the mongo service is not 330 // installed. 331 ErrMongoServiceNotInstalled = errors.ConstError("mongo service not installed") 332 // ErrMongoServiceNotRunning is returned when the mongo service is not 333 // running. 334 ErrMongoServiceNotRunning = errors.ConstError("mongo service not running") 335 ) 336 337 func ensureMongoServiceRunning(ctx context.Context, svc MongoSnapService) error { 338 return retry.Call(retry.CallArgs{ 339 Func: func() error { 340 running, err := svc.Running() 341 if err != nil { 342 // If the service is not installed, then we should attempt 343 // to install it. 344 return errors.Annotatef(ErrMongoServiceNotInstalled, err.Error()) 345 } 346 if running { 347 return nil 348 } 349 return ErrMongoServiceNotRunning 350 }, 351 Stop: ctx.Done(), 352 Attempts: 10, 353 Delay: 1 * time.Second, 354 Clock: clock.WallClock, 355 }) 356 } 357 358 func setupDataDirectory(args EnsureServerParams) error { 359 dbDir := dbDir(args.DataDir) 360 if err := os.MkdirAll(dbDir, 0700); err != nil { 361 return errors.Annotate(err, "cannot create mongo database directory") 362 } 363 364 // TODO(fix): rather than copy, we should ln -s coz it could be changed later!!! 365 if err := UpdateSSLKey(args.DataDir, args.Cert, args.PrivateKey); err != nil { 366 return errors.Trace(err) 367 } 368 369 err := utils.AtomicWriteFile(sharedSecretPath(args.DataDir), []byte(args.SharedSecret), 0600) 370 if err != nil { 371 return errors.Annotatef(err, "cannot write mongod shared secret to %v", sharedSecretPath(args.DataDir)) 372 } 373 374 if err := os.MkdirAll(logPath(dbDir), 0755); err != nil { 375 return errors.Annotate(err, "cannot create mongodb logging directory") 376 } 377 378 return nil 379 } 380 381 func truncateAndWriteIfExists(procFile, value string) error { 382 if _, err := os.Stat(procFile); os.IsNotExist(err) { 383 logger.Debugf("%q does not exist, will not set %q", procFile, value) 384 return errors.Errorf("%q does not exist, will not set %q", procFile, value) 385 } 386 f, err := os.OpenFile(procFile, os.O_WRONLY|os.O_TRUNC, 0600) 387 if err != nil { 388 return errors.Trace(err) 389 } 390 defer f.Close() 391 _, err = f.WriteString(value) 392 return errors.Trace(err) 393 } 394 395 func tweakSysctlForMongo(editables map[string]string) { 396 for editableFile, value := range editables { 397 if err := truncateAndWriteIfExists(editableFile, value); err != nil { 398 logger.Errorf("could not set the value of %q to %q because of: %v\n", editableFile, value, err) 399 } 400 } 401 } 402 403 // UpdateSSLKey writes a new SSL key used by mongo to validate connections from Juju controller(s) 404 func UpdateSSLKey(dataDir, cert, privateKey string) error { 405 err := utils.AtomicWriteFile(sslKeyPath(dataDir), []byte(GenerateSSLKey(cert, privateKey)), 0600) 406 return errors.Annotate(err, "cannot write SSL key") 407 } 408 409 // GenerateSSLKey combines cert and private key to generate the ssl key - server.pem. 410 func GenerateSSLKey(cert, privateKey string) string { 411 return cert + "\n" + privateKey 412 } 413 414 func logVersion(mongoPath string) { 415 cmd := exec.Command(mongoPath, "--version") 416 output, err := cmd.CombinedOutput() 417 if err != nil { 418 logger.Infof("failed to read the output from %s --version: %v", mongoPath, err) 419 return 420 } 421 logger.Debugf("using mongod: %s --version:\n%s", mongoPath, output) 422 } 423 424 func mongoSnapService(dataDir, configDir, snapChannel string) (MongoSnapService, error) { 425 snapName := JujuDbSnap 426 jujuDbLocalSnapPattern := regexp.MustCompile(`juju-db_[0-9]+\.snap`) 427 428 // If we're installing a local snap, then provide an absolute path 429 // as a snap <name>. snap install <name> will then do the Right Thing (TM). 430 files, err := os.ReadDir(path.Join(dataDir, "snap")) 431 if err == nil { 432 for _, fullFileName := range files { 433 _, fileName := path.Split(fullFileName.Name()) 434 if jujuDbLocalSnapPattern.MatchString(fileName) { 435 snapName = fullFileName.Name() 436 } 437 } 438 } 439 440 backgroundServices := []snap.BackgroundService{ 441 { 442 Name: "daemon", 443 EnableAtStartup: true, 444 }, 445 } 446 447 conf := common.Conf{ 448 Desc: ServiceName + " snap", 449 Limit: mongoULimits, 450 } 451 svc, err := newSnapService( 452 snapName, ServiceName, conf, snap.Command, configDir, snapChannel, "", backgroundServices, []snap.Installable{}) 453 return svc, errors.Trace(err) 454 } 455 456 // Override for testing. 457 var installMongo = packaging.InstallDependency 458 459 func installMongod(mongoDep packaging.Dependency, hostBase base.Base, snapSvc MongoSnapService) error { 460 // Do either a local snap install or a real install from the store. 461 if snapSvc.Name() == ServiceName { 462 // Store snap. 463 return installMongo(mongoDep, hostBase) 464 } else { 465 // Local snap. 466 return snapSvc.Install() 467 } 468 } 469 470 // dbDir returns the dir where mongo storage is. 471 func dbDir(dataDir string) string { 472 return filepath.Join(dataDir, "db") 473 } 474 475 // MongoSnapService represents a mongo snap. 476 type MongoSnapService interface { 477 Exists() (bool, error) 478 Installed() (bool, error) 479 Running() (bool, error) 480 ConfigOverride() error 481 Name() string 482 Start() error 483 Restart() error 484 Install() error 485 } 486 487 var newSnapService = func(mainSnap, serviceName string, conf common.Conf, snapPath, configDir, channel string, confinementPolicy snap.ConfinementPolicy, backgroundServices []snap.BackgroundService, prerequisites []snap.Installable) (MongoSnapService, error) { 488 return snap.NewService(mainSnap, serviceName, conf, snapPath, configDir, channel, confinementPolicy, backgroundServices, prerequisites) 489 } 490 491 // CurrentReplicasetConfig is overridden in tests. 492 var CurrentReplicasetConfig = func(session *mgo.Session) (*replicaset.Config, error) { 493 return replicaset.CurrentConfig(session) 494 }