github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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/errors"
    14  	"github.com/juju/utils/set"
    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  	return mongo.Path(mongo.InstalledVersion())
   113  }
   114  
   115  func getMongoToolPath(toolName string, stat func(name string) (os.FileInfo, error), lookPath func(file string) (string, error)) (string, error) {
   116  	mongod, err := getMongodPath()
   117  	if err != nil {
   118  		return "", errors.Annotate(err, "failed to get mongod path")
   119  	}
   120  	mongoTool := filepath.Join(filepath.Dir(mongod), toolName)
   121  
   122  	if _, err := stat(mongoTool); err == nil {
   123  		// It already exists so no need to continue.
   124  		return mongoTool, nil
   125  	}
   126  
   127  	path, err := lookPath(toolName)
   128  	if err != nil {
   129  		return "", errors.Trace(err)
   130  	}
   131  	return path, nil
   132  }
   133  
   134  type mongoDumper struct {
   135  	*DBInfo
   136  	// binPath is the path to the dump executable.
   137  	binPath string
   138  }
   139  
   140  // NewDBDumper returns a new value with a Dump method for dumping the
   141  // juju state database.
   142  func NewDBDumper(info *DBInfo) (DBDumper, error) {
   143  	mongodumpPath, err := getMongodumpPath()
   144  	if err != nil {
   145  		return nil, errors.Annotate(err, "mongodump not available")
   146  	}
   147  
   148  	dumper := mongoDumper{
   149  		DBInfo:  info,
   150  		binPath: mongodumpPath,
   151  	}
   152  	return &dumper, nil
   153  }
   154  
   155  func (md *mongoDumper) options(dumpDir string) []string {
   156  	options := []string{
   157  		"--ssl",
   158  		"--authenticationDatabase", "admin",
   159  		"--host", md.Address,
   160  		"--username", md.Username,
   161  		"--password", md.Password,
   162  		"--out", dumpDir,
   163  		"--oplog",
   164  	}
   165  	return options
   166  }
   167  
   168  func (md *mongoDumper) dump(dumpDir string) error {
   169  	options := md.options(dumpDir)
   170  	if err := runCommandFn(md.binPath, options...); err != nil {
   171  		return errors.Annotate(err, "error dumping databases")
   172  	}
   173  	return nil
   174  }
   175  
   176  // Dump dumps the juju state-related databases.  To do this we dump all
   177  // databases and then remove any ignored databases from the dump results.
   178  func (md *mongoDumper) Dump(baseDumpDir string) error {
   179  	if err := md.dump(baseDumpDir); err != nil {
   180  		return errors.Trace(err)
   181  	}
   182  
   183  	found, err := listDatabases(baseDumpDir)
   184  	if err != nil {
   185  		return errors.Trace(err)
   186  	}
   187  
   188  	// Strip the ignored database from the dump dir.
   189  	ignored := found.Difference(md.Targets)
   190  	// Admin must be removed only if the mongo version is 3.x or
   191  	// above, since 2.x will not restore properly without admin.
   192  	if md.DBInfo.MongoVersion.NewerThan(mongo.Mongo26) == -1 {
   193  		ignored.Remove("admin")
   194  	}
   195  	err = stripIgnored(ignored, baseDumpDir)
   196  	return errors.Trace(err)
   197  }
   198  
   199  // stripIgnored removes the ignored DBs from the mongo dump files.
   200  // This involves deleting DB-specific directories.
   201  //
   202  // NOTE(fwereade): the only directories we actually delete are "admin"
   203  // and "backups"; and those only if they're in the `ignored` set. I have
   204  // no idea why the code was structured this way; but I am, as requested
   205  // as usual by management, *not* fixing anything about backup beyond the
   206  // bug du jour.
   207  //
   208  // Basically, the ignored set is a filthy lie, and all the work we do to
   209  // generate it is pure obfuscation.
   210  func stripIgnored(ignored set.Strings, dumpDir string) error {
   211  	for _, dbName := range ignored.Values() {
   212  		switch dbName {
   213  		case storageDBName, "admin":
   214  			dirname := filepath.Join(dumpDir, dbName)
   215  			if err := os.RemoveAll(dirname); err != nil {
   216  				return errors.Trace(err)
   217  			}
   218  		}
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // listDatabases returns the name of each sub-directory of the dump
   225  // directory.  Each corresponds to a database dump generated by
   226  // mongodump.  Note that, while mongodump is unlikely to change behavior
   227  // in this regard, this is not a documented guaranteed behavior.
   228  func listDatabases(dumpDir string) (set.Strings, error) {
   229  	list, err := ioutil.ReadDir(dumpDir)
   230  	if err != nil {
   231  		return set.Strings{}, errors.Trace(err)
   232  	}
   233  
   234  	databases := make(set.Strings)
   235  	for _, info := range list {
   236  		if !info.IsDir() {
   237  			// Notably, oplog.bson is thus excluded here.
   238  			continue
   239  		}
   240  		databases.Add(info.Name())
   241  	}
   242  	return databases, nil
   243  }
   244  
   245  var getMongorestorePath = func() (string, error) {
   246  	return getMongoToolPath(restoreName, os.Stat, exec.LookPath)
   247  }
   248  
   249  // DBDumper is any type that dumps something to a dump dir.
   250  type DBRestorer interface {
   251  	// Dump something to dumpDir.
   252  	Restore(dumpDir string, dialInfo *mgo.DialInfo) error
   253  }
   254  
   255  type mongoRestorer struct {
   256  	*mgo.DialInfo
   257  	// binPath is the path to the dump executable.
   258  	binPath         string
   259  	tagUser         string
   260  	tagUserPassword string
   261  	runCommandFn    func(string, ...string) error
   262  }
   263  type mongoRestorer32 struct {
   264  	mongoRestorer
   265  	getDB           func(string, MongoSession) MongoDB
   266  	newMongoSession func(*mgo.DialInfo) (MongoSession, error)
   267  }
   268  
   269  type mongoRestorer24 struct {
   270  	mongoRestorer
   271  	stopMongo  func() error
   272  	startMongo func() error
   273  }
   274  
   275  func (md *mongoRestorer24) options(dumpDir string) []string {
   276  	dbDir := filepath.Join(agent.DefaultPaths.DataDir, "db")
   277  	options := []string{
   278  		"--drop",
   279  		"--journal",
   280  		"--oplogReplay",
   281  		"--dbpath", dbDir,
   282  		dumpDir,
   283  	}
   284  	return options
   285  }
   286  
   287  func (md *mongoRestorer24) Restore(dumpDir string, _ *mgo.DialInfo) error {
   288  	logger.Debugf("stopping mongo service for restore")
   289  	if err := md.stopMongo(); err != nil {
   290  		return errors.Annotate(err, "cannot stop mongo to replace files")
   291  	}
   292  	options := md.options(dumpDir)
   293  	logger.Infof("restoring database with params %v", options)
   294  	if err := md.runCommandFn(md.binPath, options...); err != nil {
   295  		return errors.Annotate(err, "error restoring database")
   296  	}
   297  	if err := md.startMongo(); err != nil {
   298  		return errors.Annotate(err, "cannot start mongo after restore")
   299  	}
   300  
   301  	return nil
   302  }
   303  
   304  // GetDB wraps mgo.Session.DB to ease testing.
   305  func GetDB(s string, session MongoSession) MongoDB {
   306  	return session.DB(s)
   307  }
   308  
   309  // NewMongoSession wraps mgo.DialInfo to ease testing.
   310  func NewMongoSession(dialInfo *mgo.DialInfo) (MongoSession, error) {
   311  	return mgo.DialWithInfo(dialInfo)
   312  }
   313  
   314  type RestorerArgs struct {
   315  	DialInfo        *mgo.DialInfo
   316  	NewMongoSession func(*mgo.DialInfo) (MongoSession, error)
   317  	Version         mongo.Version
   318  	TagUser         string
   319  	TagUserPassword string
   320  	GetDB           func(string, MongoSession) MongoDB
   321  
   322  	RunCommandFn func(string, ...string) error
   323  	StartMongo   func() error
   324  	StopMongo    func() error
   325  }
   326  
   327  var mongoInstalledVersion = mongo.InstalledVersion
   328  
   329  // NewDBRestorer returns a new structure that can perform a restore
   330  // on the db pointed in dialInfo.
   331  func NewDBRestorer(args RestorerArgs) (DBRestorer, error) {
   332  	mongorestorePath, err := getMongorestorePath()
   333  	if err != nil {
   334  		return nil, errors.Annotate(err, "mongorestrore not available")
   335  	}
   336  
   337  	installedMongo := mongoInstalledVersion()
   338  	logger.Debugf("args: is %#v", args)
   339  	logger.Infof("installed mongo is %s", installedMongo)
   340  	// NewerThan will check Major and Minor so migration between micro versions
   341  	// will work, before changing this bewar, Mongo has been known to break
   342  	// compatibility between minors.
   343  	if args.Version.NewerThan(installedMongo) != 0 {
   344  		return nil, errors.NotSupportedf("restore mongo version %s into version %s", args.Version.String(), installedMongo.String())
   345  	}
   346  
   347  	var restorer DBRestorer
   348  	mgoRestorer := mongoRestorer{
   349  		DialInfo:        args.DialInfo,
   350  		binPath:         mongorestorePath,
   351  		tagUser:         args.TagUser,
   352  		tagUserPassword: args.TagUserPassword,
   353  		runCommandFn:    args.RunCommandFn,
   354  	}
   355  	switch args.Version.Major {
   356  	case 2:
   357  		restorer = &mongoRestorer24{
   358  			mongoRestorer: mgoRestorer,
   359  			startMongo:    args.StartMongo,
   360  			stopMongo:     args.StopMongo,
   361  		}
   362  	case 3:
   363  		restorer = &mongoRestorer32{
   364  			mongoRestorer:   mgoRestorer,
   365  			getDB:           args.GetDB,
   366  			newMongoSession: args.NewMongoSession,
   367  		}
   368  	default:
   369  		return nil, errors.Errorf("cannot restore from mongo version %q", args.Version.String())
   370  	}
   371  	return restorer, nil
   372  }
   373  
   374  func (md *mongoRestorer32) options(dumpDir string) []string {
   375  	// note the batchSize, which is known to mitigate EOF errors
   376  	// seen when using mongorestore; as seen and reported in
   377  	// https://jira.mongodb.org/browse/TOOLS-939 -- not guaranteed
   378  	// to *help* with lp:1605653, but observed not to hurt.
   379  	//
   380  	// The value of 10 was chosen because it's more pessimistic
   381  	// than the "1000" that many report success using in the bug.
   382  	options := []string{
   383  		"--ssl",
   384  		"--authenticationDatabase", "admin",
   385  		"--host", md.Addrs[0],
   386  		"--username", md.Username,
   387  		"--password", md.Password,
   388  		"--drop",
   389  		"--oplogReplay",
   390  		"--batchSize", "10",
   391  		dumpDir,
   392  	}
   393  	return options
   394  }
   395  
   396  // MongoDB represents a mgo.DB.
   397  type MongoDB interface {
   398  	UpsertUser(*mgo.User) error
   399  }
   400  
   401  // MongoSession represents mgo.Session.
   402  type MongoSession interface {
   403  	Run(cmd interface{}, result interface{}) error
   404  	Close()
   405  	DB(string) *mgo.Database
   406  }
   407  
   408  // ensureOplogPermissions adds a special role to the admin user, this role
   409  // is required by mongorestore when doing oplogreplay.
   410  func (md *mongoRestorer32) ensureOplogPermissions(dialInfo *mgo.DialInfo) error {
   411  	s, err := md.newMongoSession(dialInfo)
   412  	if err != nil {
   413  		return errors.Trace(err)
   414  	}
   415  	defer s.Close()
   416  
   417  	roles := bson.D{
   418  		{"createRole", "oploger"},
   419  		{"privileges", []bson.D{
   420  			bson.D{
   421  				{"resource", bson.M{"anyResource": true}},
   422  				{"actions", []string{"anyAction"}},
   423  			},
   424  		}},
   425  		{"roles", []string{}},
   426  	}
   427  	var mgoErr bson.M
   428  	err = s.Run(roles, &mgoErr)
   429  	if err != nil {
   430  		return errors.Trace(err)
   431  	}
   432  	result, ok := mgoErr["ok"]
   433  	success, isFloat := result.(float64)
   434  	if (!ok || !isFloat || success != 1) && mgoErr != nil {
   435  		return errors.Errorf("could not create special role to replay oplog, result was: %#v", mgoErr)
   436  	}
   437  
   438  	// This will replace old user with the new credentials
   439  	admin := md.getDB("admin", s)
   440  
   441  	grant := bson.D{
   442  		{"grantRolesToUser", md.DialInfo.Username},
   443  		{"roles", []string{"oploger"}},
   444  	}
   445  
   446  	err = s.Run(grant, &mgoErr)
   447  	if err != nil {
   448  		return errors.Trace(err)
   449  	}
   450  	result, ok = mgoErr["ok"]
   451  	success, isFloat = result.(float64)
   452  	if (!ok || !isFloat || success != 1) && mgoErr != nil {
   453  		return errors.Errorf("could not grant special role to %q, result was: %#v", md.DialInfo.Username, mgoErr)
   454  	}
   455  
   456  	grant = bson.D{
   457  		{"grantRolesToUser", "admin"},
   458  		{"roles", []string{"oploger"}},
   459  	}
   460  
   461  	err = s.Run(grant, &mgoErr)
   462  	if err != nil {
   463  		return errors.Trace(err)
   464  	}
   465  	result, ok = mgoErr["ok"]
   466  	success, isFloat = result.(float64)
   467  	if (!ok || !isFloat || success != 1) && mgoErr != nil {
   468  		return errors.Errorf("could not grant special role to \"admin\", result was: %#v", mgoErr)
   469  	}
   470  
   471  	if err := admin.UpsertUser(&mgo.User{
   472  		Username: md.DialInfo.Username,
   473  		Password: md.DialInfo.Password,
   474  	}); err != nil {
   475  		return errors.Errorf("cannot set new admin credentials: %v", err)
   476  	}
   477  
   478  	return nil
   479  }
   480  
   481  func (md *mongoRestorer32) ensureTagUser() error {
   482  	s, err := md.newMongoSession(md.DialInfo)
   483  	if err != nil {
   484  		return errors.Trace(err)
   485  	}
   486  	defer s.Close()
   487  
   488  	admin := md.getDB("admin", s)
   489  
   490  	if err := admin.UpsertUser(&mgo.User{
   491  		Username: md.tagUser,
   492  		Password: md.tagUserPassword,
   493  	}); err != nil {
   494  		return fmt.Errorf("cannot set tag user credentials: %v", err)
   495  	}
   496  	return nil
   497  }
   498  
   499  func (md *mongoRestorer32) Restore(dumpDir string, dialInfo *mgo.DialInfo) error {
   500  	logger.Debugf("start restore, dumpDir %s", dumpDir)
   501  	if err := md.ensureOplogPermissions(dialInfo); err != nil {
   502  		return errors.Annotate(err, "setting special user permission in db")
   503  	}
   504  
   505  	options := md.options(dumpDir)
   506  	logger.Infof("restoring database with params %v", options)
   507  	if err := md.runCommandFn(md.binPath, options...); err != nil {
   508  		return errors.Annotate(err, "error restoring database")
   509  	}
   510  	logger.Infof("updating user credentials")
   511  	if err := md.ensureTagUser(); err != nil {
   512  		return errors.Trace(err)
   513  	}
   514  	return nil
   515  }