github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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  
    15  	"github.com/juju/juju/agent"
    16  	"github.com/juju/juju/juju/paths"
    17  	"github.com/juju/juju/mongo"
    18  	"github.com/juju/juju/state/imagestorage"
    19  	"github.com/juju/juju/utils"
    20  	"github.com/juju/juju/version"
    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 runCommand = utils.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  }
    46  
    47  // ignoredDatabases is the list of databases that should not be
    48  // backed up.
    49  var ignoredDatabases = set.NewStrings(
    50  	storageDBName,
    51  	"presence",
    52  	imagestorage.ImagesDB,
    53  )
    54  
    55  type DBSession interface {
    56  	DatabaseNames() ([]string, error)
    57  }
    58  
    59  // NewDBInfo returns the information needed by backups to dump
    60  // the database.
    61  func NewDBInfo(mgoInfo *mongo.MongoInfo, session DBSession) (*DBInfo, error) {
    62  	targets, err := getBackupTargetDatabases(session)
    63  	if err != nil {
    64  		return nil, errors.Trace(err)
    65  	}
    66  
    67  	info := DBInfo{
    68  		Address:  mgoInfo.Addrs[0],
    69  		Password: mgoInfo.Password,
    70  		Targets:  targets,
    71  	}
    72  
    73  	// TODO(dfc) Backup should take a Tag.
    74  	if mgoInfo.Tag != nil {
    75  		info.Username = mgoInfo.Tag.String()
    76  	}
    77  
    78  	return &info, nil
    79  }
    80  
    81  func getBackupTargetDatabases(session DBSession) (set.Strings, error) {
    82  	dbNames, err := session.DatabaseNames()
    83  	if err != nil {
    84  		return nil, errors.Annotate(err, "unable to get DB names")
    85  	}
    86  
    87  	targets := set.NewStrings(dbNames...).Difference(ignoredDatabases)
    88  	return targets, nil
    89  }
    90  
    91  const dumpName = "mongodump"
    92  
    93  // DBDumper is any type that dumps something to a dump dir.
    94  type DBDumper interface {
    95  	// Dump something to dumpDir.
    96  	Dump(dumpDir string) error
    97  }
    98  
    99  var getMongodumpPath = func() (string, error) {
   100  	mongod, err := mongo.Path()
   101  	if err != nil {
   102  		return "", errors.Annotate(err, "failed to get mongod path")
   103  	}
   104  	mongoDumpPath := filepath.Join(filepath.Dir(mongod), dumpName)
   105  
   106  	if _, err := os.Stat(mongoDumpPath); err == nil {
   107  		// It already exists so no need to continue.
   108  		return mongoDumpPath, nil
   109  	}
   110  
   111  	path, err := exec.LookPath(dumpName)
   112  	if err != nil {
   113  		return "", errors.Trace(err)
   114  	}
   115  	return path, nil
   116  }
   117  
   118  type mongoDumper struct {
   119  	*DBInfo
   120  	// binPath is the path to the dump executable.
   121  	binPath string
   122  }
   123  
   124  // NewDBDumper returns a new value with a Dump method for dumping the
   125  // juju state database.
   126  func NewDBDumper(info *DBInfo) (DBDumper, error) {
   127  	mongodumpPath, err := getMongodumpPath()
   128  	if err != nil {
   129  		return nil, errors.Annotate(err, "mongodump not available")
   130  	}
   131  
   132  	dumper := mongoDumper{
   133  		DBInfo:  info,
   134  		binPath: mongodumpPath,
   135  	}
   136  	return &dumper, nil
   137  }
   138  
   139  func (md *mongoDumper) options(dumpDir string) []string {
   140  	options := []string{
   141  		"--ssl",
   142  		"--journal",
   143  		"--authenticationDatabase", "admin",
   144  		"--host", md.Address,
   145  		"--username", md.Username,
   146  		"--password", md.Password,
   147  		"--out", dumpDir,
   148  		"--oplog",
   149  	}
   150  	return options
   151  }
   152  
   153  func (md *mongoDumper) dump(dumpDir string) error {
   154  	options := md.options(dumpDir)
   155  	if err := runCommand(md.binPath, options...); err != nil {
   156  		return errors.Annotate(err, "error dumping databases")
   157  	}
   158  	return nil
   159  }
   160  
   161  // Dump dumps the juju state-related databases.  To do this we dump all
   162  // databases and then remove any ignored databases from the dump results.
   163  func (md *mongoDumper) Dump(baseDumpDir string) error {
   164  	if err := md.dump(baseDumpDir); err != nil {
   165  		return errors.Trace(err)
   166  	}
   167  
   168  	found, err := listDatabases(baseDumpDir)
   169  	if err != nil {
   170  		return errors.Trace(err)
   171  	}
   172  
   173  	// Strip the ignored database from the dump dir.
   174  	ignored := found.Difference(md.Targets)
   175  	err = stripIgnored(ignored, baseDumpDir)
   176  	return errors.Trace(err)
   177  }
   178  
   179  // stripIgnored removes the ignored DBs from the mongo dump files.
   180  // This involves deleting DB-specific directories.
   181  func stripIgnored(ignored set.Strings, dumpDir string) error {
   182  	for _, dbName := range ignored.Values() {
   183  		if dbName != "backups" {
   184  			// We allow all ignored databases except "backups" to be
   185  			// included in the archive file.  Restore will be
   186  			// responsible for deleting those databases after
   187  			// restoring them.
   188  			continue
   189  		}
   190  		dirname := filepath.Join(dumpDir, dbName)
   191  		if err := os.RemoveAll(dirname); err != nil {
   192  			return errors.Trace(err)
   193  		}
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // listDatabases returns the name of each sub-directory of the dump
   200  // directory.  Each corresponds to a database dump generated by
   201  // mongodump.  Note that, while mongodump is unlikely to change behavior
   202  // in this regard, this is not a documented guaranteed behavior.
   203  func listDatabases(dumpDir string) (set.Strings, error) {
   204  	list, err := ioutil.ReadDir(dumpDir)
   205  	if err != nil {
   206  		return set.Strings{}, errors.Trace(err)
   207  	}
   208  
   209  	databases := make(set.Strings)
   210  	for _, info := range list {
   211  		if !info.IsDir() {
   212  			// Notably, oplog.bson is thus excluded here.
   213  			continue
   214  		}
   215  		databases.Add(info.Name())
   216  	}
   217  	return databases, nil
   218  }
   219  
   220  // mongoRestoreArgsForVersion returns a string slice containing the args to be used
   221  // to call mongo restore since these can change depending on the backup method.
   222  // Version 0: a dump made with --db, stopping the state server.
   223  // Version 1: a dump made with --oplog with a running state server.
   224  // TODO (perrito666) change versions to use metadata version
   225  func mongoRestoreArgsForVersion(ver version.Number, dumpPath string) ([]string, error) {
   226  	dbDir := filepath.Join(agent.DefaultDataDir, "db")
   227  	switch {
   228  	case ver.Major == 1 && ver.Minor < 22:
   229  		return []string{"--drop", "--journal", "--dbpath", dbDir, dumpPath}, nil
   230  	case ver.Major == 1 && ver.Minor >= 22:
   231  		return []string{"--drop", "--journal", "--oplogReplay", "--dbpath", dbDir, dumpPath}, nil
   232  	default:
   233  		return nil, errors.Errorf("this backup file is incompatible with the current version of juju")
   234  	}
   235  }
   236  
   237  var restorePath = paths.MongorestorePath
   238  var restoreArgsForVersion = mongoRestoreArgsForVersion
   239  
   240  // placeNewMongo tries to use mongorestore to replace an existing
   241  // mongo with the dump in newMongoDumpPath returns an error if its not possible.
   242  func placeNewMongo(newMongoDumpPath string, ver version.Number) error {
   243  	mongoRestore, err := restorePath()
   244  	if err != nil {
   245  		return errors.Annotate(err, "mongorestore not available")
   246  	}
   247  
   248  	mgoRestoreArgs, err := restoreArgsForVersion(ver, newMongoDumpPath)
   249  	if err != nil {
   250  		return errors.Errorf("cannot restore this backup version")
   251  	}
   252  	err = runCommand("initctl", "stop", mongo.ServiceName(""))
   253  	if err != nil {
   254  		return errors.Annotate(err, "failed to stop mongo")
   255  	}
   256  
   257  	err = runCommand(mongoRestore, mgoRestoreArgs...)
   258  
   259  	if err != nil {
   260  		return errors.Annotate(err, "failed to restore database dump")
   261  	}
   262  
   263  	err = runCommand("initctl", "start", mongo.ServiceName(""))
   264  	if err != nil {
   265  		return errors.Annotate(err, "failed to start mongo")
   266  	}
   267  
   268  	return nil
   269  }