github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  		"--authenticationDatabase", "admin",
   143  		"--host", md.Address,
   144  		"--username", md.Username,
   145  		"--password", md.Password,
   146  		"--out", dumpDir,
   147  		"--oplog",
   148  	}
   149  	return options
   150  }
   151  
   152  func (md *mongoDumper) dump(dumpDir string) error {
   153  	options := md.options(dumpDir)
   154  	if err := runCommand(md.binPath, options...); err != nil {
   155  		return errors.Annotate(err, "error dumping databases")
   156  	}
   157  	return nil
   158  }
   159  
   160  // Dump dumps the juju state-related databases.  To do this we dump all
   161  // databases and then remove any ignored databases from the dump results.
   162  func (md *mongoDumper) Dump(baseDumpDir string) error {
   163  	if err := md.dump(baseDumpDir); err != nil {
   164  		return errors.Trace(err)
   165  	}
   166  
   167  	found, err := listDatabases(baseDumpDir)
   168  	if err != nil {
   169  		return errors.Trace(err)
   170  	}
   171  
   172  	// Strip the ignored database from the dump dir.
   173  	ignored := found.Difference(md.Targets)
   174  	err = stripIgnored(ignored, baseDumpDir)
   175  	return errors.Trace(err)
   176  }
   177  
   178  // stripIgnored removes the ignored DBs from the mongo dump files.
   179  // This involves deleting DB-specific directories.
   180  func stripIgnored(ignored set.Strings, dumpDir string) error {
   181  	for _, dbName := range ignored.Values() {
   182  		if dbName != "backups" {
   183  			// We allow all ignored databases except "backups" to be
   184  			// included in the archive file.  Restore will be
   185  			// responsible for deleting those databases after
   186  			// restoring them.
   187  			continue
   188  		}
   189  		dirname := filepath.Join(dumpDir, dbName)
   190  		if err := os.RemoveAll(dirname); err != nil {
   191  			return errors.Trace(err)
   192  		}
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  // listDatabases returns the name of each sub-directory of the dump
   199  // directory.  Each corresponds to a database dump generated by
   200  // mongodump.  Note that, while mongodump is unlikely to change behavior
   201  // in this regard, this is not a documented guaranteed behavior.
   202  func listDatabases(dumpDir string) (set.Strings, error) {
   203  	list, err := ioutil.ReadDir(dumpDir)
   204  	if err != nil {
   205  		return set.Strings{}, errors.Trace(err)
   206  	}
   207  
   208  	databases := make(set.Strings)
   209  	for _, info := range list {
   210  		if !info.IsDir() {
   211  			// Notably, oplog.bson is thus excluded here.
   212  			continue
   213  		}
   214  		databases.Add(info.Name())
   215  	}
   216  	return databases, nil
   217  }
   218  
   219  // mongoRestoreArgsForVersion returns a string slice containing the args to be used
   220  // to call mongo restore since these can change depending on the backup method.
   221  // Version 0: a dump made with --db, stopping the state server.
   222  // Version 1: a dump made with --oplog with a running state server.
   223  // TODO (perrito666) change versions to use metadata version
   224  func mongoRestoreArgsForVersion(ver version.Number, dumpPath string) ([]string, error) {
   225  	dbDir := filepath.Join(agent.DefaultDataDir, "db")
   226  	switch {
   227  	case ver.Major == 1 && ver.Minor < 22:
   228  		return []string{"--drop", "--dbpath", dbDir, dumpPath}, nil
   229  	case ver.Major == 1 && ver.Minor >= 22:
   230  		return []string{"--drop", "--oplogReplay", "--dbpath", dbDir, dumpPath}, nil
   231  	default:
   232  		return nil, errors.Errorf("this backup file is incompatible with the current version of juju")
   233  	}
   234  }
   235  
   236  var restorePath = paths.MongorestorePath
   237  var restoreArgsForVersion = mongoRestoreArgsForVersion
   238  
   239  // placeNewMongo tries to use mongorestore to replace an existing
   240  // mongo with the dump in newMongoDumpPath returns an error if its not possible.
   241  func placeNewMongo(newMongoDumpPath string, ver version.Number) error {
   242  	mongoRestore, err := restorePath()
   243  	if err != nil {
   244  		return errors.Annotate(err, "mongorestore not available")
   245  	}
   246  
   247  	mgoRestoreArgs, err := restoreArgsForVersion(ver, newMongoDumpPath)
   248  	if err != nil {
   249  		return errors.Errorf("cannot restore this backup version")
   250  	}
   251  	err = runCommand("initctl", "stop", mongo.ServiceName(""))
   252  	if err != nil {
   253  		return errors.Annotate(err, "failed to stop mongo")
   254  	}
   255  
   256  	err = runCommand(mongoRestore, mgoRestoreArgs...)
   257  
   258  	if err != nil {
   259  		return errors.Annotate(err, "failed to restore database dump")
   260  	}
   261  
   262  	err = runCommand("initctl", "start", mongo.ServiceName(""))
   263  	if err != nil {
   264  		return errors.Annotate(err, "failed to start mongo")
   265  	}
   266  
   267  	return nil
   268  }