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