github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/mongo/service.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package mongo
     5  
     6  import (
     7  	"fmt"
     8  	"path/filepath"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/clock"
    15  	"github.com/juju/collections/set"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/utils/v3"
    18  
    19  	"github.com/juju/juju/network"
    20  )
    21  
    22  const (
    23  	// ServiceName is the name of the service that Juju's mongod instance
    24  	// will be named.
    25  	ServiceName = "juju-db"
    26  
    27  	// SharedSecretFile is the name of the Mongo shared secret file
    28  	// located within the Juju data directory.
    29  	SharedSecretFile = "shared-secret"
    30  
    31  	// ReplicaSetName is the name of the replica set that juju uses for its
    32  	// controllers.
    33  	ReplicaSetName = "juju"
    34  
    35  	// LowCacheSize expressed in GB sets the max value Mongo WiredTiger cache can
    36  	// reach, down to 256MB.
    37  	LowCacheSize = 0.25
    38  
    39  	// flagMarker is an in-line comment for bash. If it somehow makes its way onto
    40  	// the command line, it will be ignored. See https://stackoverflow.com/a/1456019/395287
    41  	flagMarker = "`#flag: true` \\"
    42  
    43  	dataPathForJujuDbSnap = "/var/snap/juju-db/common"
    44  
    45  	// FileNameDBSSLKey is the file name of db ssl key file name.
    46  	FileNameDBSSLKey = "server.pem"
    47  )
    48  
    49  // See https://docs.mongodb.com/manual/reference/ulimit/.
    50  var mongoULimits = map[string]string{
    51  	"fsize":   "unlimited", // file size
    52  	"cpu":     "unlimited", // cpu time
    53  	"as":      "unlimited", // virtual memory size
    54  	"memlock": "unlimited", // locked-in-memory size
    55  	"nofile":  "64000",     // open files
    56  	"nproc":   "64000",     // processes/threads
    57  }
    58  
    59  func sslKeyPath(dataDir string) string {
    60  	return filepath.Join(dataDir, FileNameDBSSLKey)
    61  }
    62  
    63  func sharedSecretPath(dataDir string) string {
    64  	return filepath.Join(dataDir, SharedSecretFile)
    65  }
    66  
    67  func logPath(dataDir string) string {
    68  	return filepath.Join(dataDir, "logs", "mongodb.log")
    69  }
    70  
    71  func configPath(dataDir string) string {
    72  	return filepath.Join(dataDir, "juju-db.config")
    73  }
    74  
    75  // ConfigArgs holds the attributes of a service configuration for mongo.
    76  type ConfigArgs struct {
    77  	Clock clock.Clock
    78  
    79  	DataDir    string
    80  	DBDir      string
    81  	ReplicaSet string
    82  
    83  	// connection params
    84  	BindIP      string
    85  	BindToAllIP bool
    86  	Port        int
    87  	OplogSizeMB int
    88  
    89  	// auth
    90  	AuthKeyFile    string
    91  	PEMKeyFile     string
    92  	PEMKeyPassword string
    93  
    94  	// network params
    95  	IPv6             bool
    96  	TLSOnNormalPorts bool
    97  	TLSMode          string
    98  
    99  	// Logging. Syslog cannot be true with LogPath set to a non-empty string.
   100  	// SlowMS is the threshold time in milliseconds that Mongo will consider an
   101  	// operation to be slow, causing it to be written to the log.
   102  	Syslog  bool
   103  	LogPath string
   104  	SlowMS  int
   105  
   106  	// db kernel
   107  	MemoryProfile         MemoryProfile
   108  	WiredTigerCacheSizeGB float32
   109  
   110  	// misc
   111  	Quiet bool
   112  }
   113  
   114  type configArgsConverter map[string]string
   115  
   116  func (conf configArgsConverter) asMongoDbConfigurationFileFormat() string {
   117  	pathArgs := set.NewStrings("dbpath", "logpath", "tlsCertificateKeyFile", "keyFile")
   118  	command := make([]string, 0, len(conf))
   119  	var keys []string
   120  	for k := range conf {
   121  		keys = append(keys, k)
   122  	}
   123  	sort.Strings(keys)
   124  	for _, key := range keys {
   125  		value := conf[key]
   126  		if len(key) == 0 {
   127  			continue
   128  		}
   129  		if pathArgs.Contains(key) {
   130  			value = strings.Trim(value, " '")
   131  		}
   132  		if value == flagMarker {
   133  			value = "true"
   134  		}
   135  		line := fmt.Sprintf("%s = %s", key, value)
   136  		if strings.HasPrefix(key, "tlsCertificateKeyFilePassword") {
   137  			line = key
   138  		}
   139  		command = append(command, line)
   140  	}
   141  
   142  	return strings.Join(command, "\n")
   143  }
   144  
   145  func (mongoArgs *ConfigArgs) asMap() configArgsConverter {
   146  	result := configArgsConverter{}
   147  	result["replSet"] = mongoArgs.ReplicaSet
   148  	result["dbpath"] = utils.ShQuote(mongoArgs.DBDir)
   149  
   150  	if mongoArgs.LogPath != "" {
   151  		result["logpath"] = utils.ShQuote(mongoArgs.LogPath)
   152  	}
   153  
   154  	if mongoArgs.BindIP != "" {
   155  		result["bind_ip"] = mongoArgs.BindIP
   156  	}
   157  	if mongoArgs.Port != 0 {
   158  		result["port"] = strconv.Itoa(mongoArgs.Port)
   159  	}
   160  	if mongoArgs.IPv6 {
   161  		result["ipv6"] = flagMarker
   162  	}
   163  	if mongoArgs.BindToAllIP {
   164  		result["bind_ip_all"] = flagMarker
   165  	}
   166  	if mongoArgs.TLSMode != "" {
   167  		result["tlsMode"] = mongoArgs.TLSMode
   168  	}
   169  	if mongoArgs.TLSOnNormalPorts {
   170  		result["tlsOnNormalPorts"] = flagMarker
   171  	}
   172  
   173  	// authn
   174  	if mongoArgs.PEMKeyFile != "" {
   175  		result["tlsCertificateKeyFile"] = utils.ShQuote(mongoArgs.PEMKeyFile)
   176  		//--tlsCertificateKeyFilePassword must be concatenated to the equals sign (lp:1581284)
   177  		pemPassword := mongoArgs.PEMKeyPassword
   178  		if pemPassword == "" {
   179  			pemPassword = "ignored"
   180  		}
   181  		result["tlsCertificateKeyFilePassword="+pemPassword] = flagMarker
   182  	}
   183  
   184  	if mongoArgs.AuthKeyFile != "" {
   185  		result["auth"] = flagMarker
   186  		result["keyFile"] = utils.ShQuote(mongoArgs.AuthKeyFile)
   187  	} else {
   188  		logger.Warningf("configuring mongod  with --noauth flag enabled")
   189  		result["noauth"] = flagMarker
   190  	}
   191  
   192  	// ops config
   193  	result["journal"] = flagMarker
   194  	if mongoArgs.OplogSizeMB != 0 {
   195  		result["oplogSize"] = strconv.Itoa(mongoArgs.OplogSizeMB)
   196  	}
   197  
   198  	result["storageEngine"] = string(WiredTiger)
   199  	if mongoArgs.WiredTigerCacheSizeGB > 0.0 {
   200  		result["wiredTigerCacheSizeGB"] = fmt.Sprint(mongoArgs.WiredTigerCacheSizeGB)
   201  	}
   202  
   203  	// Logging
   204  	if mongoArgs.Syslog {
   205  		result["syslog"] = flagMarker
   206  	}
   207  	if mongoArgs.SlowMS != 0 {
   208  		result["slowms"] = strconv.Itoa(mongoArgs.SlowMS)
   209  	}
   210  
   211  	// misc
   212  	if mongoArgs.Quiet {
   213  		result["quiet"] = flagMarker
   214  	}
   215  
   216  	return result
   217  }
   218  
   219  func (mongoArgs *ConfigArgs) writeConfig(path string) error {
   220  	generatedAt := mongoArgs.Clock.Now().UTC().Format(time.RFC822)
   221  	configPrologue := fmt.Sprintf(`
   222  # WARNING
   223  # autogenerated by juju on %v
   224  # manual changes to this file are likely to be overwritten
   225  `[1:], generatedAt)
   226  	configBody := mongoArgs.asMap().asMongoDbConfigurationFileFormat()
   227  	config := []byte(configPrologue + configBody)
   228  
   229  	err := utils.AtomicWriteFile(path, config, 0644)
   230  	if err != nil {
   231  		return errors.Annotate(err, fmt.Sprintf("writingconfig to %s", path))
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  // Override for testing.
   238  var supportsIPv6 = network.SupportsIPv6
   239  
   240  // newMongoDBArgsWithDefaults returns *mongoDbConfigArgs
   241  // under the assumption that MongoDB 3.4 or later is running.
   242  func generateConfig(oplogSizeMB int, args EnsureServerParams) *ConfigArgs {
   243  	useLowMemory := args.MemoryProfile == MemoryProfileLow
   244  
   245  	mongoArgs := &ConfigArgs{
   246  		Clock:         clock.WallClock,
   247  		DataDir:       args.DataDir,
   248  		DBDir:         dbDir(args.DataDir),
   249  		LogPath:       logPath(args.DataDir),
   250  		Port:          args.StatePort,
   251  		OplogSizeMB:   oplogSizeMB,
   252  		IPv6:          supportsIPv6(),
   253  		MemoryProfile: args.MemoryProfile,
   254  		// Switch from syslog to appending to dataDir, because snaps don't
   255  		// have the same permissions.
   256  		Syslog: false,
   257  		// SlowMS defaults to 100. This appears to log excessively.
   258  		SlowMS:           1000,
   259  		Quiet:            true,
   260  		ReplicaSet:       ReplicaSetName,
   261  		AuthKeyFile:      sharedSecretPath(args.DataDir),
   262  		PEMKeyFile:       sslKeyPath(args.DataDir),
   263  		PEMKeyPassword:   "ignored", // used as boilerplate later
   264  		TLSOnNormalPorts: false,
   265  		TLSMode:          "requireTLS",
   266  		BindToAllIP:      true, // TODO(tsm): disable when not needed
   267  		//BindIP:         "127.0.0.1", // TODO(tsm): use machine's actual IP address via dialInfo
   268  	}
   269  
   270  	if useLowMemory {
   271  		mongoArgs.WiredTigerCacheSizeGB = LowCacheSize
   272  	}
   273  	return mongoArgs
   274  }