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 }