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 }