github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "fmt" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 13 "github.com/juju/errors" 14 "github.com/juju/utils/set" 15 "gopkg.in/mgo.v2" 16 "gopkg.in/mgo.v2/bson" 17 18 "github.com/juju/juju/agent" 19 "github.com/juju/juju/mongo" 20 "github.com/juju/juju/state/imagestorage" 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 runCommandFn = 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 // MongoVersion the version of the running mongo db. 46 MongoVersion mongo.Version 47 } 48 49 // ignoredDatabases is the list of databases that should not be 50 // backed up, admin might be removed later, after determining 51 // mongo version. 52 var ignoredDatabases = set.NewStrings( 53 "admin", 54 storageDBName, 55 "presence", // note: this is still backed up anyway 56 imagestorage.ImagesDB, // note: this is still backed up anyway 57 ) 58 59 type DBSession interface { 60 DatabaseNames() ([]string, error) 61 } 62 63 // NewDBInfo returns the information needed by backups to dump 64 // the database. 65 func NewDBInfo(mgoInfo *mongo.MongoInfo, session DBSession, version mongo.Version) (*DBInfo, error) { 66 targets, err := getBackupTargetDatabases(session) 67 if err != nil { 68 return nil, errors.Trace(err) 69 } 70 71 info := DBInfo{ 72 Address: mgoInfo.Addrs[0], 73 Password: mgoInfo.Password, 74 Targets: targets, 75 MongoVersion: version, 76 } 77 78 // TODO(dfc) Backup should take a Tag. 79 if mgoInfo.Tag != nil { 80 info.Username = mgoInfo.Tag.String() 81 } 82 83 return &info, nil 84 } 85 86 func getBackupTargetDatabases(session DBSession) (set.Strings, error) { 87 dbNames, err := session.DatabaseNames() 88 if err != nil { 89 return nil, errors.Annotate(err, "unable to get DB names") 90 } 91 92 targets := set.NewStrings(dbNames...).Difference(ignoredDatabases) 93 return targets, nil 94 } 95 96 const ( 97 dumpName = "mongodump" 98 restoreName = "mongorestore" 99 ) 100 101 // DBDumper is any type that dumps something to a dump dir. 102 type DBDumper interface { 103 // Dump something to dumpDir. 104 Dump(dumpDir string) error 105 } 106 107 var getMongodumpPath = func() (string, error) { 108 return getMongoToolPath(dumpName, os.Stat, exec.LookPath) 109 } 110 111 var getMongodPath = func() (string, error) { 112 return mongo.Path(mongo.InstalledVersion()) 113 } 114 115 func getMongoToolPath(toolName string, stat func(name string) (os.FileInfo, error), lookPath func(file string) (string, error)) (string, error) { 116 mongod, err := getMongodPath() 117 if err != nil { 118 return "", errors.Annotate(err, "failed to get mongod path") 119 } 120 mongoTool := filepath.Join(filepath.Dir(mongod), toolName) 121 122 if _, err := stat(mongoTool); err == nil { 123 // It already exists so no need to continue. 124 return mongoTool, nil 125 } 126 127 path, err := lookPath(toolName) 128 if err != nil { 129 return "", errors.Trace(err) 130 } 131 return path, nil 132 } 133 134 type mongoDumper struct { 135 *DBInfo 136 // binPath is the path to the dump executable. 137 binPath string 138 } 139 140 // NewDBDumper returns a new value with a Dump method for dumping the 141 // juju state database. 142 func NewDBDumper(info *DBInfo) (DBDumper, error) { 143 mongodumpPath, err := getMongodumpPath() 144 if err != nil { 145 return nil, errors.Annotate(err, "mongodump not available") 146 } 147 148 dumper := mongoDumper{ 149 DBInfo: info, 150 binPath: mongodumpPath, 151 } 152 return &dumper, nil 153 } 154 155 func (md *mongoDumper) options(dumpDir string) []string { 156 options := []string{ 157 "--ssl", 158 "--authenticationDatabase", "admin", 159 "--host", md.Address, 160 "--username", md.Username, 161 "--password", md.Password, 162 "--out", dumpDir, 163 "--oplog", 164 } 165 return options 166 } 167 168 func (md *mongoDumper) dump(dumpDir string) error { 169 options := md.options(dumpDir) 170 if err := runCommandFn(md.binPath, options...); err != nil { 171 return errors.Annotate(err, "error dumping databases") 172 } 173 return nil 174 } 175 176 // Dump dumps the juju state-related databases. To do this we dump all 177 // databases and then remove any ignored databases from the dump results. 178 func (md *mongoDumper) Dump(baseDumpDir string) error { 179 if err := md.dump(baseDumpDir); err != nil { 180 return errors.Trace(err) 181 } 182 183 found, err := listDatabases(baseDumpDir) 184 if err != nil { 185 return errors.Trace(err) 186 } 187 188 // Strip the ignored database from the dump dir. 189 ignored := found.Difference(md.Targets) 190 // Admin must be removed only if the mongo version is 3.x or 191 // above, since 2.x will not restore properly without admin. 192 if md.DBInfo.MongoVersion.NewerThan(mongo.Mongo26) == -1 { 193 ignored.Remove("admin") 194 } 195 err = stripIgnored(ignored, baseDumpDir) 196 return errors.Trace(err) 197 } 198 199 // stripIgnored removes the ignored DBs from the mongo dump files. 200 // This involves deleting DB-specific directories. 201 // 202 // NOTE(fwereade): the only directories we actually delete are "admin" 203 // and "backups"; and those only if they're in the `ignored` set. I have 204 // no idea why the code was structured this way; but I am, as requested 205 // as usual by management, *not* fixing anything about backup beyond the 206 // bug du jour. 207 // 208 // Basically, the ignored set is a filthy lie, and all the work we do to 209 // generate it is pure obfuscation. 210 func stripIgnored(ignored set.Strings, dumpDir string) error { 211 for _, dbName := range ignored.Values() { 212 switch dbName { 213 case storageDBName, "admin": 214 dirname := filepath.Join(dumpDir, dbName) 215 if err := os.RemoveAll(dirname); err != nil { 216 return errors.Trace(err) 217 } 218 } 219 } 220 221 return nil 222 } 223 224 // listDatabases returns the name of each sub-directory of the dump 225 // directory. Each corresponds to a database dump generated by 226 // mongodump. Note that, while mongodump is unlikely to change behavior 227 // in this regard, this is not a documented guaranteed behavior. 228 func listDatabases(dumpDir string) (set.Strings, error) { 229 list, err := ioutil.ReadDir(dumpDir) 230 if err != nil { 231 return set.Strings{}, errors.Trace(err) 232 } 233 234 databases := make(set.Strings) 235 for _, info := range list { 236 if !info.IsDir() { 237 // Notably, oplog.bson is thus excluded here. 238 continue 239 } 240 databases.Add(info.Name()) 241 } 242 return databases, nil 243 } 244 245 var getMongorestorePath = func() (string, error) { 246 return getMongoToolPath(restoreName, os.Stat, exec.LookPath) 247 } 248 249 // DBDumper is any type that dumps something to a dump dir. 250 type DBRestorer interface { 251 // Dump something to dumpDir. 252 Restore(dumpDir string, dialInfo *mgo.DialInfo) error 253 } 254 255 type mongoRestorer struct { 256 *mgo.DialInfo 257 // binPath is the path to the dump executable. 258 binPath string 259 tagUser string 260 tagUserPassword string 261 runCommandFn func(string, ...string) error 262 } 263 type mongoRestorer32 struct { 264 mongoRestorer 265 getDB func(string, MongoSession) MongoDB 266 newMongoSession func(*mgo.DialInfo) (MongoSession, error) 267 } 268 269 type mongoRestorer24 struct { 270 mongoRestorer 271 stopMongo func() error 272 startMongo func() error 273 } 274 275 func (md *mongoRestorer24) options(dumpDir string) []string { 276 dbDir := filepath.Join(agent.DefaultPaths.DataDir, "db") 277 options := []string{ 278 "--drop", 279 "--journal", 280 "--oplogReplay", 281 "--dbpath", dbDir, 282 dumpDir, 283 } 284 return options 285 } 286 287 func (md *mongoRestorer24) Restore(dumpDir string, _ *mgo.DialInfo) error { 288 logger.Debugf("stopping mongo service for restore") 289 if err := md.stopMongo(); err != nil { 290 return errors.Annotate(err, "cannot stop mongo to replace files") 291 } 292 options := md.options(dumpDir) 293 logger.Infof("restoring database with params %v", options) 294 if err := md.runCommandFn(md.binPath, options...); err != nil { 295 return errors.Annotate(err, "error restoring database") 296 } 297 if err := md.startMongo(); err != nil { 298 return errors.Annotate(err, "cannot start mongo after restore") 299 } 300 301 return nil 302 } 303 304 // GetDB wraps mgo.Session.DB to ease testing. 305 func GetDB(s string, session MongoSession) MongoDB { 306 return session.DB(s) 307 } 308 309 // NewMongoSession wraps mgo.DialInfo to ease testing. 310 func NewMongoSession(dialInfo *mgo.DialInfo) (MongoSession, error) { 311 return mgo.DialWithInfo(dialInfo) 312 } 313 314 type RestorerArgs struct { 315 DialInfo *mgo.DialInfo 316 NewMongoSession func(*mgo.DialInfo) (MongoSession, error) 317 Version mongo.Version 318 TagUser string 319 TagUserPassword string 320 GetDB func(string, MongoSession) MongoDB 321 322 RunCommandFn func(string, ...string) error 323 StartMongo func() error 324 StopMongo func() error 325 } 326 327 var mongoInstalledVersion = mongo.InstalledVersion 328 329 // NewDBRestorer returns a new structure that can perform a restore 330 // on the db pointed in dialInfo. 331 func NewDBRestorer(args RestorerArgs) (DBRestorer, error) { 332 mongorestorePath, err := getMongorestorePath() 333 if err != nil { 334 return nil, errors.Annotate(err, "mongorestrore not available") 335 } 336 337 installedMongo := mongoInstalledVersion() 338 logger.Debugf("args: is %#v", args) 339 logger.Infof("installed mongo is %s", installedMongo) 340 // NewerThan will check Major and Minor so migration between micro versions 341 // will work, before changing this bewar, Mongo has been known to break 342 // compatibility between minors. 343 if args.Version.NewerThan(installedMongo) != 0 { 344 return nil, errors.NotSupportedf("restore mongo version %s into version %s", args.Version.String(), installedMongo.String()) 345 } 346 347 var restorer DBRestorer 348 mgoRestorer := mongoRestorer{ 349 DialInfo: args.DialInfo, 350 binPath: mongorestorePath, 351 tagUser: args.TagUser, 352 tagUserPassword: args.TagUserPassword, 353 runCommandFn: args.RunCommandFn, 354 } 355 switch args.Version.Major { 356 case 2: 357 restorer = &mongoRestorer24{ 358 mongoRestorer: mgoRestorer, 359 startMongo: args.StartMongo, 360 stopMongo: args.StopMongo, 361 } 362 case 3: 363 restorer = &mongoRestorer32{ 364 mongoRestorer: mgoRestorer, 365 getDB: args.GetDB, 366 newMongoSession: args.NewMongoSession, 367 } 368 default: 369 return nil, errors.Errorf("cannot restore from mongo version %q", args.Version.String()) 370 } 371 return restorer, nil 372 } 373 374 func (md *mongoRestorer32) options(dumpDir string) []string { 375 // note the batchSize, which is known to mitigate EOF errors 376 // seen when using mongorestore; as seen and reported in 377 // https://jira.mongodb.org/browse/TOOLS-939 -- not guaranteed 378 // to *help* with lp:1605653, but observed not to hurt. 379 // 380 // The value of 10 was chosen because it's more pessimistic 381 // than the "1000" that many report success using in the bug. 382 options := []string{ 383 "--ssl", 384 "--authenticationDatabase", "admin", 385 "--host", md.Addrs[0], 386 "--username", md.Username, 387 "--password", md.Password, 388 "--drop", 389 "--oplogReplay", 390 "--batchSize", "10", 391 dumpDir, 392 } 393 return options 394 } 395 396 // MongoDB represents a mgo.DB. 397 type MongoDB interface { 398 UpsertUser(*mgo.User) error 399 } 400 401 // MongoSession represents mgo.Session. 402 type MongoSession interface { 403 Run(cmd interface{}, result interface{}) error 404 Close() 405 DB(string) *mgo.Database 406 } 407 408 // ensureOplogPermissions adds a special role to the admin user, this role 409 // is required by mongorestore when doing oplogreplay. 410 func (md *mongoRestorer32) ensureOplogPermissions(dialInfo *mgo.DialInfo) error { 411 s, err := md.newMongoSession(dialInfo) 412 if err != nil { 413 return errors.Trace(err) 414 } 415 defer s.Close() 416 417 roles := bson.D{ 418 {"createRole", "oploger"}, 419 {"privileges", []bson.D{ 420 bson.D{ 421 {"resource", bson.M{"anyResource": true}}, 422 {"actions", []string{"anyAction"}}, 423 }, 424 }}, 425 {"roles", []string{}}, 426 } 427 var mgoErr bson.M 428 err = s.Run(roles, &mgoErr) 429 if err != nil { 430 return errors.Trace(err) 431 } 432 result, ok := mgoErr["ok"] 433 success, isFloat := result.(float64) 434 if (!ok || !isFloat || success != 1) && mgoErr != nil { 435 return errors.Errorf("could not create special role to replay oplog, result was: %#v", mgoErr) 436 } 437 438 // This will replace old user with the new credentials 439 admin := md.getDB("admin", s) 440 441 grant := bson.D{ 442 {"grantRolesToUser", md.DialInfo.Username}, 443 {"roles", []string{"oploger"}}, 444 } 445 446 err = s.Run(grant, &mgoErr) 447 if err != nil { 448 return errors.Trace(err) 449 } 450 result, ok = mgoErr["ok"] 451 success, isFloat = result.(float64) 452 if (!ok || !isFloat || success != 1) && mgoErr != nil { 453 return errors.Errorf("could not grant special role to %q, result was: %#v", md.DialInfo.Username, mgoErr) 454 } 455 456 grant = bson.D{ 457 {"grantRolesToUser", "admin"}, 458 {"roles", []string{"oploger"}}, 459 } 460 461 err = s.Run(grant, &mgoErr) 462 if err != nil { 463 return errors.Trace(err) 464 } 465 result, ok = mgoErr["ok"] 466 success, isFloat = result.(float64) 467 if (!ok || !isFloat || success != 1) && mgoErr != nil { 468 return errors.Errorf("could not grant special role to \"admin\", result was: %#v", mgoErr) 469 } 470 471 if err := admin.UpsertUser(&mgo.User{ 472 Username: md.DialInfo.Username, 473 Password: md.DialInfo.Password, 474 }); err != nil { 475 return errors.Errorf("cannot set new admin credentials: %v", err) 476 } 477 478 return nil 479 } 480 481 func (md *mongoRestorer32) ensureTagUser() error { 482 s, err := md.newMongoSession(md.DialInfo) 483 if err != nil { 484 return errors.Trace(err) 485 } 486 defer s.Close() 487 488 admin := md.getDB("admin", s) 489 490 if err := admin.UpsertUser(&mgo.User{ 491 Username: md.tagUser, 492 Password: md.tagUserPassword, 493 }); err != nil { 494 return fmt.Errorf("cannot set tag user credentials: %v", err) 495 } 496 return nil 497 } 498 499 func (md *mongoRestorer32) Restore(dumpDir string, dialInfo *mgo.DialInfo) error { 500 logger.Debugf("start restore, dumpDir %s", dumpDir) 501 if err := md.ensureOplogPermissions(dialInfo); err != nil { 502 return errors.Annotate(err, "setting special user permission in db") 503 } 504 505 options := md.options(dumpDir) 506 logger.Infof("restoring database with params %v", options) 507 if err := md.runCommandFn(md.binPath, options...); err != nil { 508 return errors.Annotate(err, "error restoring database") 509 } 510 logger.Infof("updating user credentials") 511 if err := md.ensureTagUser(); err != nil { 512 return errors.Trace(err) 513 } 514 return nil 515 }