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 }