github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils/set"
    14  	"github.com/juju/version"
    15  
    16  	"github.com/juju/juju/agent"
    17  	"github.com/juju/juju/mongo"
    18  	"github.com/juju/juju/state/imagestorage"
    19  )
    20  
    21  // db is a surrogate for the proverbial DB layer abstraction that we
    22  // wish we had for juju state.  To that end, the package holds the DB
    23  // implementation-specific details and functionality needed for backups.
    24  // Currently that means mongo-specific details.  However, as a stand-in
    25  // for a future DB layer abstraction, the db package does not expose any
    26  // low-level details publicly.  Thus the backups implementation remains
    27  // oblivious to the underlying DB implementation.
    28  
    29  var runCommandFn = runCommand
    30  
    31  // DBInfo wraps all the DB-specific information backups needs to dump
    32  // the database. This includes a simplification of the information in
    33  // authentication.MongoInfo.
    34  type DBInfo struct {
    35  	// Address is the DB system's host address.
    36  	Address string
    37  	// Username is used when connecting to the DB system.
    38  	Username string
    39  	// Password is used when connecting to the DB system.
    40  	Password string
    41  	// Targets is a list of databases to dump.
    42  	Targets set.Strings
    43  }
    44  
    45  // ignoredDatabases is the list of databases that should not be
    46  // backed up.
    47  var ignoredDatabases = set.NewStrings(
    48  	storageDBName,
    49  	"presence",
    50  	imagestorage.ImagesDB,
    51  )
    52  
    53  type DBSession interface {
    54  	DatabaseNames() ([]string, error)
    55  }
    56  
    57  // NewDBInfo returns the information needed by backups to dump
    58  // the database.
    59  func NewDBInfo(mgoInfo *mongo.MongoInfo, session DBSession) (*DBInfo, error) {
    60  	targets, err := getBackupTargetDatabases(session)
    61  	if err != nil {
    62  		return nil, errors.Trace(err)
    63  	}
    64  
    65  	info := DBInfo{
    66  		Address:  mgoInfo.Addrs[0],
    67  		Password: mgoInfo.Password,
    68  		Targets:  targets,
    69  	}
    70  
    71  	// TODO(dfc) Backup should take a Tag.
    72  	if mgoInfo.Tag != nil {
    73  		info.Username = mgoInfo.Tag.String()
    74  	}
    75  
    76  	return &info, nil
    77  }
    78  
    79  func getBackupTargetDatabases(session DBSession) (set.Strings, error) {
    80  	dbNames, err := session.DatabaseNames()
    81  	if err != nil {
    82  		return nil, errors.Annotate(err, "unable to get DB names")
    83  	}
    84  
    85  	targets := set.NewStrings(dbNames...).Difference(ignoredDatabases)
    86  	return targets, nil
    87  }
    88  
    89  const (
    90  	dumpName    = "mongodump"
    91  	restoreName = "mongorestore"
    92  )
    93  
    94  // DBDumper is any type that dumps something to a dump dir.
    95  type DBDumper interface {
    96  	// Dump something to dumpDir.
    97  	Dump(dumpDir string) error
    98  }
    99  
   100  var getMongodumpPath = func() (string, error) {
   101  	return getMongoToolPath(dumpName, os.Stat, exec.LookPath)
   102  }
   103  
   104  var getMongodPath = func() (string, error) {
   105  	return mongo.Path(mongo.InstalledVersion())
   106  }
   107  
   108  func getMongoToolPath(toolName string, stat func(name string) (os.FileInfo, error), lookPath func(file string) (string, error)) (string, error) {
   109  	mongod, err := getMongodPath()
   110  	if err != nil {
   111  		return "", errors.Annotate(err, "failed to get mongod path")
   112  	}
   113  	mongoTool := filepath.Join(filepath.Dir(mongod), toolName)
   114  
   115  	if _, err := stat(mongoTool); err == nil {
   116  		// It already exists so no need to continue.
   117  		return mongoTool, nil
   118  	}
   119  
   120  	path, err := lookPath(toolName)
   121  	if err != nil {
   122  		return "", errors.Trace(err)
   123  	}
   124  	return path, nil
   125  }
   126  
   127  type mongoDumper struct {
   128  	*DBInfo
   129  	// binPath is the path to the dump executable.
   130  	binPath string
   131  }
   132  
   133  // NewDBDumper returns a new value with a Dump method for dumping the
   134  // juju state database.
   135  func NewDBDumper(info *DBInfo) (DBDumper, error) {
   136  	mongodumpPath, err := getMongodumpPath()
   137  	if err != nil {
   138  		return nil, errors.Annotate(err, "mongodump not available")
   139  	}
   140  
   141  	dumper := mongoDumper{
   142  		DBInfo:  info,
   143  		binPath: mongodumpPath,
   144  	}
   145  	return &dumper, nil
   146  }
   147  
   148  func (md *mongoDumper) options(dumpDir string) []string {
   149  	options := []string{
   150  		"--ssl",
   151  		"--authenticationDatabase", "admin",
   152  		"--host", md.Address,
   153  		"--username", md.Username,
   154  		"--password", md.Password,
   155  		"--out", dumpDir,
   156  		"--oplog",
   157  	}
   158  	return options
   159  }
   160  
   161  func (md *mongoDumper) dump(dumpDir string) error {
   162  	options := md.options(dumpDir)
   163  	if err := runCommandFn(md.binPath, options...); err != nil {
   164  		return errors.Annotate(err, "error dumping databases")
   165  	}
   166  	return nil
   167  }
   168  
   169  // Dump dumps the juju state-related databases.  To do this we dump all
   170  // databases and then remove any ignored databases from the dump results.
   171  func (md *mongoDumper) Dump(baseDumpDir string) error {
   172  	if err := md.dump(baseDumpDir); err != nil {
   173  		return errors.Trace(err)
   174  	}
   175  
   176  	found, err := listDatabases(baseDumpDir)
   177  	if err != nil {
   178  		return errors.Trace(err)
   179  	}
   180  
   181  	// Strip the ignored database from the dump dir.
   182  	ignored := found.Difference(md.Targets)
   183  	err = stripIgnored(ignored, baseDumpDir)
   184  	return errors.Trace(err)
   185  }
   186  
   187  // stripIgnored removes the ignored DBs from the mongo dump files.
   188  // This involves deleting DB-specific directories.
   189  func stripIgnored(ignored set.Strings, dumpDir string) error {
   190  	for _, dbName := range ignored.Values() {
   191  		if dbName != "backups" {
   192  			// We allow all ignored databases except "backups" to be
   193  			// included in the archive file.  Restore will be
   194  			// responsible for deleting those databases after
   195  			// restoring them.
   196  			continue
   197  		}
   198  		dirname := filepath.Join(dumpDir, dbName)
   199  		if err := os.RemoveAll(dirname); err != nil {
   200  			return errors.Trace(err)
   201  		}
   202  	}
   203  
   204  	return nil
   205  }
   206  
   207  // listDatabases returns the name of each sub-directory of the dump
   208  // directory.  Each corresponds to a database dump generated by
   209  // mongodump.  Note that, while mongodump is unlikely to change behavior
   210  // in this regard, this is not a documented guaranteed behavior.
   211  func listDatabases(dumpDir string) (set.Strings, error) {
   212  	list, err := ioutil.ReadDir(dumpDir)
   213  	if err != nil {
   214  		return set.Strings{}, errors.Trace(err)
   215  	}
   216  
   217  	databases := make(set.Strings)
   218  	for _, info := range list {
   219  		if !info.IsDir() {
   220  			// Notably, oplog.bson is thus excluded here.
   221  			continue
   222  		}
   223  		databases.Add(info.Name())
   224  	}
   225  	return databases, nil
   226  }
   227  
   228  // mongoRestoreArgsForVersion returns a string slice containing the args to be used
   229  // to call mongo restore since these can change depending on the backup method.
   230  // Version 0: a dump made with --db, stopping the controller.
   231  // Version 1: a dump made with --oplog with a running controller.
   232  // TODO (perrito666) change versions to use metadata version
   233  func mongoRestoreArgsForVersion(ver version.Number, dumpPath string) ([]string, error) {
   234  	dbDir := filepath.Join(agent.DefaultPaths.DataDir, "db")
   235  	switch {
   236  	case ver.Major == 1 && ver.Minor < 22:
   237  		return []string{"--drop", "--journal", "--dbpath", dbDir, dumpPath}, nil
   238  	case ver.Major == 1 && ver.Minor >= 22,
   239  		ver.Major == 2:
   240  		return []string{"--drop", "--journal", "--oplogReplay", "--dbpath", dbDir, dumpPath}, nil
   241  	default:
   242  		return nil, errors.Errorf("this backup file is incompatible with the current version of juju")
   243  	}
   244  }
   245  
   246  var getMongorestorePath = func() (string, error) {
   247  	return getMongoToolPath(restoreName, os.Stat, exec.LookPath)
   248  }
   249  
   250  var restoreArgsForVersion = mongoRestoreArgsForVersion
   251  
   252  // placeNewMongoService wraps placeNewMongo with the proper service stopping
   253  // and starting before dumping the new mongo db, it is mainly to easy testing
   254  // of placeNewMongo.
   255  func placeNewMongoService(newMongoDumpPath string, ver version.Number) error {
   256  	err := mongo.StopService()
   257  	if err != nil {
   258  		return errors.Annotate(err, "failed to stop mongo")
   259  	}
   260  
   261  	if err := placeNewMongo(newMongoDumpPath, ver); err != nil {
   262  		return errors.Annotate(err, "cannot place new mongo")
   263  	}
   264  	err = mongo.StartService()
   265  	return errors.Annotate(err, "failed to start mongo")
   266  }
   267  
   268  // placeNewMongo tries to use mongorestore to replace an existing
   269  // mongo with the dump in newMongoDumpPath returns an error if its not possible.
   270  func placeNewMongo(newMongoDumpPath string, ver version.Number) error {
   271  	mongoRestore, err := getMongorestorePath()
   272  	if err != nil {
   273  		return errors.Annotate(err, "mongorestore not available")
   274  	}
   275  
   276  	mgoRestoreArgs, err := restoreArgsForVersion(ver, newMongoDumpPath)
   277  	if err != nil {
   278  		return errors.Errorf("cannot restore this backup version")
   279  	}
   280  
   281  	err = runCommandFn(mongoRestore, mgoRestoreArgs...)
   282  
   283  	if err != nil {
   284  		return errors.Annotate(err, "failed to restore database dump")
   285  	}
   286  
   287  	return nil
   288  }