github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/state/backups/db.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package backups
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  
    13  	"github.com/juju/collections/set"
    14  	"github.com/juju/errors"
    15  	"gopkg.in/mgo.v2"
    16  	"gopkg.in/mgo.v2/bson"
    17  
    18  	"github.com/juju/juju/agent"
    19  	"github.com/juju/juju/mongo"
    20  	"github.com/juju/juju/state/imagestorage"
    21  )
    22  
    23  // db is a surrogate for the proverbial DB layer abstraction that we
    24  // wish we had for juju state.  To that end, the package holds the DB
    25  // implementation-specific details and functionality needed for backups.
    26  // Currently that means mongo-specific details.  However, as a stand-in
    27  // for a future DB layer abstraction, the db package does not expose any
    28  // low-level details publicly.  Thus the backups implementation remains
    29  // oblivious to the underlying DB implementation.
    30  
    31  var runCommandFn = runCommand
    32  
    33  // DBInfo wraps all the DB-specific information backups needs to dump
    34  // the database. This includes a simplification of the information in
    35  // authentication.MongoInfo.
    36  type DBInfo struct {
    37  	// Address is the DB system's host address.
    38  	Address string
    39  	// Username is used when connecting to the DB system.
    40  	Username string
    41  	// Password is used when connecting to the DB system.
    42  	Password string
    43  	// Targets is a list of databases to dump.
    44  	Targets set.Strings
    45  	// MongoVersion the version of the running mongo db.
    46  	MongoVersion mongo.Version
    47  }
    48  
    49  // ignoredDatabases is the list of databases that should not be
    50  // backed up, admin might be removed later, after determining
    51  // mongo version.
    52  var ignoredDatabases = set.NewStrings(
    53  	"admin",
    54  	storageDBName,
    55  	"presence",            // note: this is still backed up anyway
    56  	imagestorage.ImagesDB, // note: this is still backed up anyway
    57  )
    58  
    59  type DBSession interface {
    60  	DatabaseNames() ([]string, error)
    61  }
    62  
    63  // NewDBInfo returns the information needed by backups to dump
    64  // the database.
    65  func NewDBInfo(mgoInfo *mongo.MongoInfo, session DBSession, version mongo.Version) (*DBInfo, error) {
    66  	targets, err := getBackupTargetDatabases(session)
    67  	if err != nil {
    68  		return nil, errors.Trace(err)
    69  	}
    70  
    71  	info := DBInfo{
    72  		Address:      mgoInfo.Addrs[0],
    73  		Password:     mgoInfo.Password,
    74  		Targets:      targets,
    75  		MongoVersion: version,
    76  	}
    77  
    78  	// TODO(dfc) Backup should take a Tag.
    79  	if mgoInfo.Tag != nil {
    80  		info.Username = mgoInfo.Tag.String()
    81  	}
    82  
    83  	return &info, nil
    84  }
    85  
    86  func getBackupTargetDatabases(session DBSession) (set.Strings, error) {
    87  	dbNames, err := session.DatabaseNames()
    88  	if err != nil {
    89  		return nil, errors.Annotate(err, "unable to get DB names")
    90  	}
    91  
    92  	targets := set.NewStrings(dbNames...).Difference(ignoredDatabases)
    93  	return targets, nil
    94  }
    95  
    96  const (
    97  	dumpName    = "mongodump"
    98  	restoreName = "mongorestore"
    99  )
   100  
   101  // DBDumper is any type that dumps something to a dump dir.
   102  type DBDumper interface {
   103  	// Dump something to dumpDir.
   104  	Dump(dumpDir string) error
   105  }
   106  
   107  var getMongodumpPath = func() (string, error) {
   108  	return getMongoToolPath(dumpName, os.Stat, exec.LookPath)
   109  }
   110  
   111  var getMongodPath = func() (string, error) {
   112  	finder := mongo.NewMongodFinder()
   113  	path, _, err := finder.FindBest()
   114  	return path, err
   115  }
   116  
   117  func getMongoToolPath(toolName string, stat func(name string) (os.FileInfo, error), lookPath func(file string) (string, error)) (string, error) {
   118  	mongod, err := getMongodPath()
   119  	if err != nil {
   120  		return "", errors.Annotate(err, "failed to get mongod path")
   121  	}
   122  	mongoTool := filepath.Join(filepath.Dir(mongod), toolName)
   123  
   124  	if _, err := stat(mongoTool); err == nil {
   125  		// It already exists so no need to continue.
   126  		return mongoTool, nil
   127  	}
   128  
   129  	path, err := lookPath(toolName)
   130  	if err != nil {
   131  		return "", errors.Trace(err)
   132  	}
   133  	return path, nil
   134  }
   135  
   136  type mongoDumper struct {
   137  	*DBInfo
   138  	// binPath is the path to the dump executable.
   139  	binPath string
   140  }
   141  
   142  // NewDBDumper returns a new value with a Dump method for dumping the
   143  // juju state database.
   144  func NewDBDumper(info *DBInfo) (DBDumper, error) {
   145  	mongodumpPath, err := getMongodumpPath()
   146  	if err != nil {
   147  		return nil, errors.Annotate(err, "mongodump not available")
   148  	}
   149  
   150  	dumper := mongoDumper{
   151  		DBInfo:  info,
   152  		binPath: mongodumpPath,
   153  	}
   154  	return &dumper, nil
   155  }
   156  
   157  func (md *mongoDumper) options(dumpDir string) []string {
   158  	options := []string{
   159  		"--ssl",
   160  		"--sslAllowInvalidCertificates",
   161  		"--authenticationDatabase", "admin",
   162  		"--host", md.Address,
   163  		"--username", md.Username,
   164  		"--password", md.Password,
   165  		"--out", dumpDir,
   166  		"--oplog",
   167  	}
   168  	return options
   169  }
   170  
   171  func (md *mongoDumper) dump(dumpDir string) error {
   172  	options := md.options(dumpDir)
   173  	if err := runCommandFn(md.binPath, options...); err != nil {
   174  		return errors.Annotate(err, "error dumping databases")
   175  	}
   176  	return nil
   177  }
   178  
   179  // Dump dumps the juju state-related databases.  To do this we dump all
   180  // databases and then remove any ignored databases from the dump results.
   181  func (md *mongoDumper) Dump(baseDumpDir string) error {
   182  	if err := md.dump(baseDumpDir); err != nil {
   183  		return errors.Trace(err)
   184  	}
   185  
   186  	found, err := listDatabases(baseDumpDir)
   187  	if err != nil {
   188  		return errors.Trace(err)
   189  	}
   190  
   191  	// Strip the ignored database from the dump dir.
   192  	ignored := found.Difference(md.Targets)
   193  	// Admin must be removed only if the mongo version is 3.x or
   194  	// above, since 2.x will not restore properly without admin.
   195  	if md.DBInfo.MongoVersion.NewerThan(mongo.Mongo26) == -1 {
   196  		ignored.Remove("admin")
   197  	}
   198  	err = stripIgnored(ignored, baseDumpDir)
   199  	return errors.Trace(err)
   200  }
   201  
   202  // stripIgnored removes the ignored DBs from the mongo dump files.
   203  // This involves deleting DB-specific directories.
   204  //
   205  // NOTE(fwereade): the only directories we actually delete are "admin"
   206  // and "backups"; and those only if they're in the `ignored` set. I have
   207  // no idea why the code was structured this way; but I am, as requested
   208  // as usual by management, *not* fixing anything about backup beyond the
   209  // bug du jour.
   210  //
   211  // Basically, the ignored set is a filthy lie, and all the work we do to
   212  // generate it is pure obfuscation.
   213  func stripIgnored(ignored set.Strings, dumpDir string) error {
   214  	for _, dbName := range ignored.Values() {
   215  		switch dbName {
   216  		case storageDBName, "admin":
   217  			dirname := filepath.Join(dumpDir, dbName)
   218  			if err := os.RemoveAll(dirname); err != nil {
   219  				return errors.Trace(err)
   220  			}
   221  		}
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  // listDatabases returns the name of each sub-directory of the dump
   228  // directory.  Each corresponds to a database dump generated by
   229  // mongodump.  Note that, while mongodump is unlikely to change behavior
   230  // in this regard, this is not a documented guaranteed behavior.
   231  func listDatabases(dumpDir string) (set.Strings, error) {
   232  	list, err := ioutil.ReadDir(dumpDir)
   233  	if err != nil {
   234  		return set.Strings{}, errors.Trace(err)
   235  	}
   236  
   237  	databases := make(set.Strings)
   238  	for _, info := range list {
   239  		if !info.IsDir() {
   240  			// Notably, oplog.bson is thus excluded here.
   241  			continue
   242  		}
   243  		databases.Add(info.Name())
   244  	}
   245  	return databases, nil
   246  }
   247  
   248  var getMongorestorePath = func() (string, error) {
   249  	return getMongoToolPath(restoreName, os.Stat, exec.LookPath)
   250  }
   251  
   252  // DBDumper is any type that dumps something to a dump dir.
   253  type DBRestorer interface {
   254  	// Dump something to dumpDir.
   255  	Restore(dumpDir string, dialInfo *mgo.DialInfo) error
   256  }
   257  
   258  type mongoRestorer struct {
   259  	*mgo.DialInfo
   260  	// binPath is the path to the dump executable.
   261  	binPath         string
   262  	tagUser         string
   263  	tagUserPassword string
   264  	runCommandFn    func(string, ...string) error
   265  }
   266  type mongoRestorer32 struct {
   267  	mongoRestorer
   268  	getDB           func(string, MongoSession) MongoDB
   269  	newMongoSession func(*mgo.DialInfo) (MongoSession, error)
   270  }
   271  
   272  type mongoRestorer24 struct {
   273  	mongoRestorer
   274  	stopMongo  func() error
   275  	startMongo func() error
   276  }
   277  
   278  func (md *mongoRestorer24) options(dumpDir string) []string {
   279  	dbDir := filepath.Join(agent.DefaultPaths.DataDir, "db")
   280  	options := []string{
   281  		"--drop",
   282  		"--journal",
   283  		"--oplogReplay",
   284  		"--dbpath", dbDir,
   285  		dumpDir,
   286  	}
   287  	return options
   288  }
   289  
   290  func (md *mongoRestorer24) Restore(dumpDir string, _ *mgo.DialInfo) error {
   291  	logger.Debugf("stopping mongo service for restore")
   292  	if err := md.stopMongo(); err != nil {
   293  		return errors.Annotate(err, "cannot stop mongo to replace files")
   294  	}
   295  	options := md.options(dumpDir)
   296  	logger.Infof("restoring database with params %v", options)
   297  	if err := md.runCommandFn(md.binPath, options...); err != nil {
   298  		return errors.Annotate(err, "error restoring database")
   299  	}
   300  	if err := md.startMongo(); err != nil {
   301  		return errors.Annotate(err, "cannot start mongo after restore")
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  // GetDB wraps mgo.Session.DB to ease testing.
   308  func GetDB(s string, session MongoSession) MongoDB {
   309  	return session.DB(s)
   310  }
   311  
   312  // NewMongoSession wraps mgo.DialInfo to ease testing.
   313  func NewMongoSession(dialInfo *mgo.DialInfo) (MongoSession, error) {
   314  	return mgo.DialWithInfo(dialInfo)
   315  }
   316  
   317  type RestorerArgs struct {
   318  	DialInfo        *mgo.DialInfo
   319  	NewMongoSession func(*mgo.DialInfo) (MongoSession, error)
   320  	Version         mongo.Version
   321  	TagUser         string
   322  	TagUserPassword string
   323  	GetDB           func(string, MongoSession) MongoDB
   324  
   325  	RunCommandFn func(string, ...string) error
   326  	StartMongo   func() error
   327  	StopMongo    func() error
   328  }
   329  
   330  var mongoInstalledVersion = func() mongo.Version {
   331  	finder := mongo.NewMongodFinder()
   332  	// We ignore the error here. The old code always assumed that
   333  	// InstalledVersion always had a correct answer.
   334  	_, version, _ := finder.FindBest()
   335  	return version
   336  }
   337  
   338  // NewDBRestorer returns a new structure that can perform a restore
   339  // on the db pointed in dialInfo.
   340  func NewDBRestorer(args RestorerArgs) (DBRestorer, error) {
   341  	mongorestorePath, err := getMongorestorePath()
   342  	if err != nil {
   343  		return nil, errors.Annotate(err, "mongorestrore not available")
   344  	}
   345  
   346  	installedMongo := mongoInstalledVersion()
   347  	logger.Debugf("args: is %#v", args)
   348  	logger.Infof("installed mongo is %s", installedMongo)
   349  	// NewerThan will check Major and Minor so migration between micro versions
   350  	// will work, before changing this bewar, Mongo has been known to break
   351  	// compatibility between minors.
   352  	if args.Version.NewerThan(installedMongo) != 0 {
   353  		return nil, errors.NotSupportedf("restore mongo version %s into version %s", args.Version.String(), installedMongo.String())
   354  	}
   355  
   356  	var restorer DBRestorer
   357  	mgoRestorer := mongoRestorer{
   358  		DialInfo:        args.DialInfo,
   359  		binPath:         mongorestorePath,
   360  		tagUser:         args.TagUser,
   361  		tagUserPassword: args.TagUserPassword,
   362  		runCommandFn:    args.RunCommandFn,
   363  	}
   364  	switch args.Version.Major {
   365  	case 2:
   366  		restorer = &mongoRestorer24{
   367  			mongoRestorer: mgoRestorer,
   368  			startMongo:    args.StartMongo,
   369  			stopMongo:     args.StopMongo,
   370  		}
   371  	case 3:
   372  		restorer = &mongoRestorer32{
   373  			mongoRestorer:   mgoRestorer,
   374  			getDB:           args.GetDB,
   375  			newMongoSession: args.NewMongoSession,
   376  		}
   377  	default:
   378  		return nil, errors.Errorf("cannot restore from mongo version %q", args.Version.String())
   379  	}
   380  	return restorer, nil
   381  }
   382  
   383  func (md *mongoRestorer32) options(dumpDir string) []string {
   384  	// note the batchSize, which is known to mitigate EOF errors
   385  	// seen when using mongorestore; as seen and reported in
   386  	// https://jira.mongodb.org/browse/TOOLS-939 -- not guaranteed
   387  	// to *help* with lp:1605653, but observed not to hurt.
   388  	//
   389  	// The value of 10 was chosen because it's more pessimistic
   390  	// than the "1000" that many report success using in the bug.
   391  	options := []string{
   392  		"--ssl",
   393  		"--sslAllowInvalidCertificates",
   394  		"--authenticationDatabase", "admin",
   395  		"--host", md.Addrs[0],
   396  		"--username", md.Username,
   397  		"--password", md.Password,
   398  		"--drop",
   399  		"--oplogReplay",
   400  		"--batchSize", "10",
   401  		dumpDir,
   402  	}
   403  	return options
   404  }
   405  
   406  // MongoDB represents a mgo.DB.
   407  type MongoDB interface {
   408  	UpsertUser(*mgo.User) error
   409  }
   410  
   411  // MongoSession represents mgo.Session.
   412  type MongoSession interface {
   413  	Run(cmd interface{}, result interface{}) error
   414  	Close()
   415  	DB(string) *mgo.Database
   416  }
   417  
   418  // ensureOplogPermissions adds a special role to the admin user, this role
   419  // is required by mongorestore when doing oplogreplay.
   420  func (md *mongoRestorer32) ensureOplogPermissions(dialInfo *mgo.DialInfo) error {
   421  	s, err := md.newMongoSession(dialInfo)
   422  	if err != nil {
   423  		return errors.Trace(err)
   424  	}
   425  	defer s.Close()
   426  
   427  	roles := bson.D{
   428  		{"createRole", "oploger"},
   429  		{"privileges", []bson.D{
   430  			{
   431  				{"resource", bson.M{"anyResource": true}},
   432  				{"actions", []string{"anyAction"}},
   433  			},
   434  		}},
   435  		{"roles", []string{}},
   436  	}
   437  	var mgoErr bson.M
   438  	err = s.Run(roles, &mgoErr)
   439  	if err != nil && !mgo.IsDup(err) {
   440  		return errors.Trace(err)
   441  	}
   442  	result, ok := mgoErr["ok"]
   443  	success, isFloat := result.(float64)
   444  	if (!ok || !isFloat || success != 1) && mgoErr != nil && !mgo.IsDup(err) {
   445  		return errors.Errorf("could not create special role to replay oplog, result was: %#v", mgoErr)
   446  	}
   447  
   448  	// This will replace old user with the new credentials
   449  	admin := md.getDB("admin", s)
   450  
   451  	grant := bson.D{
   452  		{"grantRolesToUser", md.DialInfo.Username},
   453  		{"roles", []string{"oploger"}},
   454  	}
   455  
   456  	err = s.Run(grant, &mgoErr)
   457  	if err != nil {
   458  		return errors.Trace(err)
   459  	}
   460  	result, ok = mgoErr["ok"]
   461  	success, isFloat = result.(float64)
   462  	if (!ok || !isFloat || success != 1) && mgoErr != nil {
   463  		return errors.Errorf("could not grant special role to %q, result was: %#v", md.DialInfo.Username, mgoErr)
   464  	}
   465  
   466  	grant = bson.D{
   467  		{"grantRolesToUser", "admin"},
   468  		{"roles", []string{"oploger"}},
   469  	}
   470  
   471  	err = s.Run(grant, &mgoErr)
   472  	if err != nil {
   473  		return errors.Trace(err)
   474  	}
   475  	result, ok = mgoErr["ok"]
   476  	success, isFloat = result.(float64)
   477  	if (!ok || !isFloat || success != 1) && mgoErr != nil {
   478  		return errors.Errorf("could not grant special role to \"admin\", result was: %#v", mgoErr)
   479  	}
   480  
   481  	if err := admin.UpsertUser(&mgo.User{
   482  		Username: md.DialInfo.Username,
   483  		Password: md.DialInfo.Password,
   484  	}); err != nil {
   485  		return errors.Errorf("cannot set new admin credentials: %v", err)
   486  	}
   487  
   488  	return nil
   489  }
   490  
   491  func (md *mongoRestorer32) ensureTagUser() error {
   492  	s, err := md.newMongoSession(md.DialInfo)
   493  	if err != nil {
   494  		return errors.Trace(err)
   495  	}
   496  	defer s.Close()
   497  
   498  	admin := md.getDB("admin", s)
   499  
   500  	if err := admin.UpsertUser(&mgo.User{
   501  		Username: md.tagUser,
   502  		Password: md.tagUserPassword,
   503  	}); err != nil {
   504  		return fmt.Errorf("cannot set tag user credentials: %v", err)
   505  	}
   506  	return nil
   507  }
   508  
   509  func (md *mongoRestorer32) Restore(dumpDir string, dialInfo *mgo.DialInfo) error {
   510  	logger.Debugf("start restore, dumpDir %s", dumpDir)
   511  	if err := md.ensureOplogPermissions(dialInfo); err != nil {
   512  		return errors.Annotate(err, "setting special user permission in db")
   513  	}
   514  
   515  	options := md.options(dumpDir)
   516  	logger.Infof("restoring database with params %v", options)
   517  	if err := md.runCommandFn(md.binPath, options...); err != nil {
   518  		return errors.Annotate(err, "error restoring database")
   519  	}
   520  	logger.Infof("updating user credentials")
   521  	if err := md.ensureTagUser(); err != nil {
   522  		return errors.Trace(err)
   523  	}
   524  	return nil
   525  }