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  }