diff options
author | David Golden <xdg@xdg.me> | 2018-05-11 12:22:02 -0400 |
---|---|---|
committer | David Golden <xdg@xdg.me> | 2018-05-11 12:22:02 -0400 |
commit | 56cbcf28c6bd4d9f9e739fb834d83b61c99516f1 (patch) | |
tree | a276b5c489cefbca00fb15293d48756df5ea4241 /src | |
parent | 737596d7005ae061809cbd8483ba91b4a2a89d86 (diff) | |
download | mongo-56cbcf28c6bd4d9f9e739fb834d83b61c99516f1.tar.gz |
Import tools: 0373beacadd1b314e2da616acefc5804889eb92c from branch v4.0
ref: 7b16532d46..0373beacad
for: 3.7.10
TOOLS-1817 Add 'preserveUUIDs' flag to restore collections with existing UUIDS
TOOLS-2002 mongorestore --oplogReplay failed on createIndexes command
TOOLS-2033 bsondump_broken_pipe.js fails intermittently in Evergreen
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/gotools/bsondump/bsondump.go | 13 | ||||
-rw-r--r-- | src/mongo/gotools/common/db/command.go | 25 | ||||
-rw-r--r-- | src/mongo/gotools/common/db/db.go | 4 | ||||
-rw-r--r-- | src/mongo/gotools/common/failpoint/failpoints.go | 1 | ||||
-rw-r--r-- | src/mongo/gotools/common/util/mongo.go | 24 | ||||
-rw-r--r-- | src/mongo/gotools/import.data | 4 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/metadata.go | 75 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/mongorestore.go | 14 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/mongorestore_test.go | 163 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/oplog.go | 62 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/oplog_test.go | 50 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/options.go | 1 | ||||
-rw-r--r-- | src/mongo/gotools/mongorestore/restore.go | 21 |
13 files changed, 411 insertions, 46 deletions
diff --git a/src/mongo/gotools/bsondump/bsondump.go b/src/mongo/gotools/bsondump/bsondump.go index 96d0baef180..3d26d5d6a5e 100644 --- a/src/mongo/gotools/bsondump/bsondump.go +++ b/src/mongo/gotools/bsondump/bsondump.go @@ -10,16 +10,19 @@ package bsondump import ( "bytes" "fmt" + "io" + "os" + "strings" + "time" + "github.com/mongodb/mongo-tools/common/bsonutil" "github.com/mongodb/mongo-tools/common/db" + "github.com/mongodb/mongo-tools/common/failpoint" "github.com/mongodb/mongo-tools/common/json" "github.com/mongodb/mongo-tools/common/log" "github.com/mongodb/mongo-tools/common/options" "github.com/mongodb/mongo-tools/common/util" "gopkg.in/mgo.v2/bson" - "io" - "os" - "strings" ) // BSONDump is a container for the user-specified options and @@ -130,6 +133,10 @@ func (bd *BSONDump) JSON() (int, error) { } } numFound++ + if failpoint.Enabled(failpoint.SlowBSONDump) { + time.Sleep(2 * time.Second) + } + } if err := decodedStream.Err(); err != nil { return numFound, err diff --git a/src/mongo/gotools/common/db/command.go b/src/mongo/gotools/common/db/command.go index 62fa834309b..d427b3b431b 100644 --- a/src/mongo/gotools/common/db/command.go +++ b/src/mongo/gotools/common/db/command.go @@ -8,9 +8,10 @@ package db import ( "fmt" + "strings" + "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" - "strings" ) // Query flags @@ -131,6 +132,28 @@ func (sp *SessionProvider) IsMongos() (bool, error) { return nodeType == Mongos, nil } +// SupportsCollectionUUID returns true if the connected server identifies +// collections with UUIDs +func (sp *SessionProvider) SupportsCollectionUUID() (bool, error) { + session, err := sp.GetSession() + if err != nil { + return false, err + } + defer session.Close() + + collInfo, err := GetCollectionInfo(session.DB("admin").C("system.version")) + if err != nil { + return false, err + } + + // On FCV 3.6+, admin.system.version will have a UUID + if collInfo != nil && collInfo.GetUUID() != "" { + return true, nil + } + + return false, nil +} + // SupportsRepairCursor takes in an example db and collection name and // returns true if the connected server supports the repairCursor command. // It returns false and the error that occurred if it is not supported. diff --git a/src/mongo/gotools/common/db/db.go b/src/mongo/gotools/common/db/db.go index 888556b97ea..839db12d491 100644 --- a/src/mongo/gotools/common/db/db.go +++ b/src/mongo/gotools/common/db/db.go @@ -94,8 +94,8 @@ type Oplog struct { Version int `bson:"v"` Operation string `bson:"op"` Namespace string `bson:"ns"` - Object bson.RawD `bson:"o"` - Query bson.RawD `bson:"o2"` + Object bson.D `bson:"o"` + Query bson.D `bson:"o2"` UI *bson.Binary `bson:"ui,omitempty"` } diff --git a/src/mongo/gotools/common/failpoint/failpoints.go b/src/mongo/gotools/common/failpoint/failpoints.go index be9aa752f10..0258ca1d2a0 100644 --- a/src/mongo/gotools/common/failpoint/failpoints.go +++ b/src/mongo/gotools/common/failpoint/failpoints.go @@ -9,4 +9,5 @@ package failpoint // Supported failpoint names const ( PauseBeforeDumping = "PauseBeforeDumping" + SlowBSONDump = "SlowBSONDump" ) diff --git a/src/mongo/gotools/common/util/mongo.go b/src/mongo/gotools/common/util/mongo.go index 80b5945d5e8..cf3168d2b68 100644 --- a/src/mongo/gotools/common/util/mongo.go +++ b/src/mongo/gotools/common/util/mongo.go @@ -62,14 +62,8 @@ func CreateConnectionAddrs(host, port string) []string { } // SplitNamespace splits a namespace path into a database and collection, -// returned in that order. An error is returned if the namespace is invalid. -func SplitAndValidateNamespace(namespace string) (string, string, error) { - - // first, run validation checks - if err := ValidateFullNamespace(namespace); err != nil { - return "", "", fmt.Errorf("namespace '%v' is not valid: %v", - namespace, err) - } +// returned in that order. +func SplitNamespace(namespace string) (string, string) { // find the first instance of "." in the namespace firstDotIndex := strings.Index(namespace, ".") @@ -84,6 +78,20 @@ func SplitAndValidateNamespace(namespace string) (string, string, error) { database = namespace } + return database, collection +} + +// SplitAndValidateNamespace splits a namespace path into a database and collection, +// returned in that order. An error is returned if the namespace is invalid. +func SplitAndValidateNamespace(namespace string) (string, string, error) { + + // first, run validation checks + if err := ValidateFullNamespace(namespace); err != nil { + return "", "", fmt.Errorf("namespace '%v' is not valid: %v", + namespace, err) + } + + database, collection := SplitNamespace(namespace) return database, collection, nil } diff --git a/src/mongo/gotools/import.data b/src/mongo/gotools/import.data index 19d8adae1e2..d4e8508aab4 100644 --- a/src/mongo/gotools/import.data +++ b/src/mongo/gotools/import.data @@ -1,6 +1,6 @@ { - "commit": "7b16532d4653bcfcb5b3c919c7610cca8035af04", + "commit": "0373beacadd1b314e2da616acefc5804889eb92c", "github": "mongodb/mongo-tools.git", "vendor": "tools", - "branch": "master" + "branch": "v4.0" } diff --git a/src/mongo/gotools/mongorestore/metadata.go b/src/mongo/gotools/mongorestore/metadata.go index 12d99d858c0..7e644f3c047 100644 --- a/src/mongo/gotools/mongorestore/metadata.go +++ b/src/mongo/gotools/mongorestore/metadata.go @@ -7,6 +7,7 @@ package mongorestore import ( + "encoding/hex" "fmt" "github.com/mongodb/mongo-tools/common" @@ -38,6 +39,7 @@ type authVersionPair struct { type Metadata struct { Options bson.D `json:"options,omitempty"` Indexes []IndexDocument `json:"indexes"` + UUID string `json:"uuid"` } // this struct is used to read in the options of a set of indexes @@ -54,17 +56,17 @@ type IndexDocument struct { // MetadataFromJSON takes a slice of JSON bytes and unmarshals them into usable // collection options and indexes for restoring collections. -func (restore *MongoRestore) MetadataFromJSON(jsonBytes []byte) (bson.D, []IndexDocument, error) { +func (restore *MongoRestore) MetadataFromJSON(jsonBytes []byte) (*Metadata, error) { if len(jsonBytes) == 0 { // skip metadata parsing if the file is empty - return nil, nil, nil + return nil, nil } meta := &Metadata{} err := json.Unmarshal(jsonBytes, meta) if err != nil { - return nil, nil, err + return nil, err } // first get the ordered key information for each index, @@ -72,7 +74,7 @@ func (restore *MongoRestore) MetadataFromJSON(jsonBytes []byte) (bson.D, []Index metaAsMap := metaDataMapIndex{} err = json.Unmarshal(jsonBytes, &metaAsMap) if err != nil { - return nil, nil, fmt.Errorf("error unmarshalling metadata as map: %v", err) + return nil, fmt.Errorf("error unmarshalling metadata as map: %v", err) } for i := range meta.Indexes { // remove "key", and "partialFilterExpression" from the map so we can decode them properly later @@ -82,21 +84,21 @@ func (restore *MongoRestore) MetadataFromJSON(jsonBytes []byte) (bson.D, []Index // parse extra index fields meta.Indexes[i].Options = metaAsMap.Indexes[i] if err := bsonutil.ConvertJSONDocumentToBSON(meta.Indexes[i].Options); err != nil { - return nil, nil, fmt.Errorf("extended json error: %v", err) + return nil, fmt.Errorf("extended json error: %v", err) } // parse the values of the index keys, so we can support extended json for pos, field := range meta.Indexes[i].Key { meta.Indexes[i].Key[pos].Value, err = bsonutil.ParseJSONValue(field.Value) if err != nil { - return nil, nil, fmt.Errorf("extended json in 'key.%v' field: %v", field.Name, err) + return nil, fmt.Errorf("extended json in 'key.%v' field: %v", field.Name, err) } } // parse the values of the index keys, so we can support extended json for pos, field := range meta.Indexes[i].PartialFilterExpression { meta.Indexes[i].PartialFilterExpression[pos].Value, err = bsonutil.ParseJSONValue(field.Value) if err != nil { - return nil, nil, fmt.Errorf("extended json in 'partialFilterExpression.%v' field: %v", field.Name, err) + return nil, fmt.Errorf("extended json in 'partialFilterExpression.%v' field: %v", field.Name, err) } } } @@ -104,10 +106,10 @@ func (restore *MongoRestore) MetadataFromJSON(jsonBytes []byte) (bson.D, []Index // parse the values of options fields, to support extended json meta.Options, err = bsonutil.GetExtendedBsonD(meta.Options) if err != nil { - return nil, nil, fmt.Errorf("extended json in 'options': %v", err) + return nil, fmt.Errorf("extended json in 'options': %v", err) } - return meta.Options, meta.Indexes, nil + return meta, nil } // LoadIndexesFromBSON reads indexes from the index BSON files and @@ -258,17 +260,27 @@ func (restore *MongoRestore) LegacyInsertIndex(intent *intents.Intent, index Ind // CreateCollection creates the collection specified in the intent with the // given options. -func (restore *MongoRestore) CreateCollection(intent *intents.Intent, options bson.D) error { - command := append(bson.D{{"create", intent.C}}, options...) - +func (restore *MongoRestore) CreateCollection(intent *intents.Intent, options bson.D, uuid string) error { session, err := restore.SessionProvider.GetSession() if err != nil { return fmt.Errorf("error establishing connection: %v", err) } defer session.Close() + switch { + + case uuid != "": + return restore.createCollectionWithApplyOps(session, intent, options, uuid) + default: + return restore.createCollectionWithCommand(session, intent, options) + } + +} + +func (restore *MongoRestore) createCollectionWithCommand(session *mgo.Session, intent *intents.Intent, options bson.D) error { + command := createCollectionCommand(intent, options) res := bson.M{} - err = session.DB(intent.DB).Run(command, &res) + err := session.DB(intent.DB).Run(command, &res) if err != nil { return fmt.Errorf("error running create command: %v", err) } @@ -276,6 +288,43 @@ func (restore *MongoRestore) CreateCollection(intent *intents.Intent, options bs return fmt.Errorf("create command: %v", res["errmsg"]) } return nil + +} + +func (restore *MongoRestore) createCollectionWithApplyOps(session *mgo.Session, intent *intents.Intent, options bson.D, uuidHex string) error { + command := createCollectionCommand(intent, options) + uuid, err := hex.DecodeString(uuidHex) + if err != nil { + return fmt.Errorf("Couldn't restore UUID because UUID was invalid: %s", err) + } + + b, err := bson.Marshal(command) + if err != nil { + return err + } + var rawCommand bson.RawD + err = bson.Unmarshal(b, &rawCommand) + if err != nil { + return err + } + + createOp := struct { + Operation string `bson:"op"` + Namespace string `bson:"ns"` + Object bson.RawD `bson:"o"` + UI *bson.Binary `bson:"ui,omitempty"` + }{ + Operation: "c", + Namespace: intent.DB + ".$cmd", + Object: rawCommand, + UI: &bson.Binary{Kind: 0x04, Data: uuid}, + } + + return restore.ApplyOps(session, []interface{}{createOp}) +} + +func createCollectionCommand(intent *intents.Intent, options bson.D) bson.D { + return append(bson.D{{"create", intent.C}}, options...) } // RestoreUsersOrRoles accepts a users intent and a roles intent, and restores diff --git a/src/mongo/gotools/mongorestore/mongorestore.go b/src/mongo/gotools/mongorestore/mongorestore.go index ad5bc33b327..bab85e31277 100644 --- a/src/mongo/gotools/mongorestore/mongorestore.go +++ b/src/mongo/gotools/mongorestore/mongorestore.go @@ -226,6 +226,20 @@ func (restore *MongoRestore) ParseAndValidateOptions() error { "cannot specify a negative number of insertion workers per collection") } + if restore.OutputOptions.PreserveUUID { + if !restore.OutputOptions.Drop { + return fmt.Errorf("cannot specify --preserveUUID without --drop") + } + + ok, err := restore.SessionProvider.SupportsCollectionUUID() + if err != nil { + return err + } + if !ok { + return fmt.Errorf("target host does not support --preserveUUID") + } + } + // a single dash signals reading from stdin if restore.TargetDirectory == "-" { if restore.InputOptions.Archive != "" { diff --git a/src/mongo/gotools/mongorestore/mongorestore_test.go b/src/mongo/gotools/mongorestore/mongorestore_test.go index 0bafb739c6b..b2a312c005c 100644 --- a/src/mongo/gotools/mongorestore/mongorestore_test.go +++ b/src/mongo/gotools/mongorestore/mongorestore_test.go @@ -86,3 +86,166 @@ func TestMongorestore(t *testing.T) { }) } + +func TestMongorestoreCantPreserveUUID(t *testing.T) { + testutil.VerifyTestType(t, testutil.IntegrationTestType) + session, err := testutil.GetBareSession() + if err != nil { + t.Fatalf("No server available") + } + defer session.Close() + fcv := testutil.GetFCV(session) + if cmp, err := testutil.CompareFCV(fcv, "3.6"); err != nil || cmp >= 0 { + t.Skip("Requires server with FCV less than 3.6") + } + + Convey("With a test MongoRestore", t, func() { + nsOptions := &NSOptions{} + provider, toolOpts, err := testutil.GetBareSessionProvider() + if err != nil { + log.Logvf(log.Always, "error connecting to host: %v", err) + os.Exit(util.ExitError) + } + + Convey("PreserveUUID restore with incompatible destination FCV errors", func() { + inputOptions := &InputOptions{} + outputOptions := &OutputOptions{ + NumParallelCollections: 1, + NumInsertionWorkers: 1, + PreserveUUID: true, + Drop: true, + } + restore := MongoRestore{ + ToolOptions: toolOpts, + OutputOptions: outputOptions, + InputOptions: inputOptions, + NSOptions: nsOptions, + SessionProvider: provider, + } + restore.TargetDirectory = "testdata/oplogdump" + err = restore.Restore() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "target host does not support --preserveUUID") + }) + }) +} + +func TestMongorestorePreserveUUID(t *testing.T) { + testutil.VerifyTestType(t, testutil.IntegrationTestType) + session, err := testutil.GetBareSession() + if err != nil { + t.Fatalf("No server available") + } + defer session.Close() + fcv := testutil.GetFCV(session) + if cmp, err := testutil.CompareFCV(fcv, "3.6"); err != nil || cmp < 0 { + t.Skip("Requires server with FCV 3.6 or later") + } + + // From mongorestore/testdata/oplogdump/db1/c1.metadata.json + originalUUID := "699f503df64b4aa8a484a8052046fa3a" + + Convey("With a test MongoRestore", t, func() { + nsOptions := &NSOptions{} + provider, toolOpts, err := testutil.GetBareSessionProvider() + if err != nil { + log.Logvf(log.Always, "error connecting to host: %v", err) + os.Exit(util.ExitError) + } + + c1 := session.DB("db1").C("c1") + c1.DropCollection() + + Convey("normal restore gives new UUID", func() { + inputOptions := &InputOptions{} + outputOptions := &OutputOptions{ + NumParallelCollections: 1, + NumInsertionWorkers: 1, + } + restore := MongoRestore{ + ToolOptions: toolOpts, + OutputOptions: outputOptions, + InputOptions: inputOptions, + NSOptions: nsOptions, + SessionProvider: provider, + } + restore.TargetDirectory = "testdata/oplogdump" + err = restore.Restore() + So(err, ShouldBeNil) + count, err := c1.Count() + So(err, ShouldBeNil) + So(count, ShouldEqual, 5) + info, err := db.GetCollectionInfo(c1) + So(err, ShouldBeNil) + So(info.GetUUID(), ShouldNotEqual, originalUUID) + }) + + Convey("PreserveUUID restore without drop errors", func() { + inputOptions := &InputOptions{} + outputOptions := &OutputOptions{ + NumParallelCollections: 1, + NumInsertionWorkers: 1, + PreserveUUID: true, + } + restore := MongoRestore{ + ToolOptions: toolOpts, + OutputOptions: outputOptions, + InputOptions: inputOptions, + NSOptions: nsOptions, + SessionProvider: provider, + } + restore.TargetDirectory = "testdata/oplogdump" + err = restore.Restore() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "cannot specify --preserveUUID without --drop") + }) + + Convey("PreserveUUID with drop preserves UUID", func() { + inputOptions := &InputOptions{} + outputOptions := &OutputOptions{ + NumParallelCollections: 1, + NumInsertionWorkers: 1, + PreserveUUID: true, + Drop: true, + } + restore := MongoRestore{ + ToolOptions: toolOpts, + OutputOptions: outputOptions, + InputOptions: inputOptions, + NSOptions: nsOptions, + SessionProvider: provider, + } + restore.TargetDirectory = "testdata/oplogdump" + err = restore.Restore() + So(err, ShouldBeNil) + count, err := c1.Count() + So(err, ShouldBeNil) + So(count, ShouldEqual, 5) + info, err := db.GetCollectionInfo(c1) + So(err, ShouldBeNil) + So(info.GetUUID(), ShouldEqual, originalUUID) + }) + + Convey("PreserveUUID on a file without UUID metadata errors", func() { + inputOptions := &InputOptions{} + outputOptions := &OutputOptions{ + NumParallelCollections: 1, + NumInsertionWorkers: 1, + PreserveUUID: true, + Drop: true, + } + restore := MongoRestore{ + ToolOptions: toolOpts, + OutputOptions: outputOptions, + InputOptions: inputOptions, + NSOptions: nsOptions, + SessionProvider: provider, + } + restore.TargetDirectory = "testdata/testdirs" + err = restore.Restore() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "--preserveUUID used but no UUID found") + }) + + }) +} diff --git a/src/mongo/gotools/mongorestore/oplog.go b/src/mongo/gotools/mongorestore/oplog.go index 94475f7dc62..00746cb8d15 100644 --- a/src/mongo/gotools/mongorestore/oplog.go +++ b/src/mongo/gotools/mongorestore/oplog.go @@ -87,8 +87,7 @@ func (restore *MongoRestore) RestoreOplog() error { break } - // TODO: TOOLS-1817 will add support for conditionally keeping UUIDS - entryAsOplog, err = filterUUIDs(entryAsOplog) + entryAsOplog, err = restore.filterUUIDs(entryAsOplog) if err != nil { return fmt.Errorf("error filtering UUIDs from oplog: %v", err) } @@ -168,13 +167,22 @@ func ParseTimestampFlag(ts string) (bson.MongoTimestamp, error) { } // filterUUIDs removes 'ui' entries from ops, including nested applyOps ops. -func filterUUIDs(op db.Oplog) (db.Oplog, error) { +// It also modifies ops that rely on 'ui'. +func (restore *MongoRestore) filterUUIDs(op db.Oplog) (db.Oplog, error) { // Remove UUIDs from oplog entries - op.UI = nil + if !restore.OutputOptions.PreserveUUID { + op.UI = nil + + // new createIndexes oplog command requires 'ui', so if we aren't + // preserving UUIDs, we must convert it to an old style index insert + if op.Operation == "c" && op.Object[0].Name == "createIndexes" { + return convertCreateIndexToIndexInsert(op) + } + } // Check for and filter nested applyOps ops if op.Operation == "c" && isApplyOpsCmd(op.Object) { - filtered, err := newFilteredApplyOps(op.Object) + filtered, err := restore.newFilteredApplyOps(op.Object) if err != nil { return db.Oplog{}, err } @@ -184,8 +192,36 @@ func filterUUIDs(op db.Oplog) (db.Oplog, error) { return op, nil } +// convertCreateIndexToIndexInsert converts from new-style create indexes +// command to old style special index insert. +func convertCreateIndexToIndexInsert(op db.Oplog) (db.Oplog, error) { + dbName, _ := util.SplitNamespace(op.Namespace) + + cmdValue := op.Object[0].Value + collName, ok := cmdValue.(string) + if !ok { + return db.Oplog{}, fmt.Errorf("unknown format for createIndexes") + } + + indexSpec := op.Object[1:] + if len(indexSpec) < 3 { + return db.Oplog{}, fmt.Errorf("unknown format for createIndexes, index spec " + + "must have at least \"v\", \"key\", and \"name\" fields") + } + + // createIndexes does not include the "ns" field but index inserts + // do. Add it as the third field, after "v", "key", and "name". + ns := bson.D{{"ns", fmt.Sprintf("%s.%s", dbName, collName)}} + indexSpec = append(indexSpec[:3], append(ns, indexSpec[3:]...)...) + op.Object = indexSpec + op.Namespace = fmt.Sprintf("%s.system.indexes", dbName) + op.Operation = "i" + + return op, nil +} + // isApplyOpsCmd returns true if a document seems to be an applyOps command. -func isApplyOpsCmd(cmd bson.RawD) bool { +func isApplyOpsCmd(cmd bson.D) bool { for _, v := range cmd { if v.Name == "applyOps" { return true @@ -196,7 +232,7 @@ func isApplyOpsCmd(cmd bson.RawD) bool { // newFilteredApplyOps iterates over nested ops in an applyOps document and // returns a new applyOps document that omits the 'ui' field from nested ops. -func newFilteredApplyOps(cmd bson.RawD) (bson.RawD, error) { +func (restore *MongoRestore) newFilteredApplyOps(cmd bson.D) (bson.D, error) { ops, err := unwrapNestedApplyOps(cmd) if err != nil { return nil, err @@ -204,7 +240,7 @@ func newFilteredApplyOps(cmd bson.RawD) (bson.RawD, error) { filtered := make([]db.Oplog, len(ops)) for i, v := range ops { - filtered[i], err = filterUUIDs(v) + filtered[i], err = restore.filterUUIDs(v) if err != nil { return nil, err } @@ -223,10 +259,10 @@ type nestedApplyOps struct { ApplyOps []db.Oplog `bson:"applyOps"` } -// unwrapNestedApplyOps converts a RawD to a typed data structure. +// unwrapNestedApplyOps converts a bson.D to a typed data structure. // Unfortunately, we're forced to convert by marshaling to bytes and // unmarshaling. -func unwrapNestedApplyOps(doc bson.RawD) ([]db.Oplog, error) { +func unwrapNestedApplyOps(doc bson.D) ([]db.Oplog, error) { // Doc to bytes bs, err := bson.Marshal(doc) if err != nil { @@ -243,10 +279,10 @@ func unwrapNestedApplyOps(doc bson.RawD) ([]db.Oplog, error) { return cmd.ApplyOps, nil } -// wrapNestedApplyOps converts a typed data structure to a RawD. +// wrapNestedApplyOps converts a typed data structure to a bson.D. // Unfortunately, we're forced to convert by marshaling to bytes and // unmarshaling. -func wrapNestedApplyOps(ops []db.Oplog) (bson.RawD, error) { +func wrapNestedApplyOps(ops []db.Oplog) (bson.D, error) { cmd := &nestedApplyOps{ApplyOps: ops} // Typed data to bytes @@ -256,7 +292,7 @@ func wrapNestedApplyOps(ops []db.Oplog) (bson.RawD, error) { } // Bytes to doc - var doc bson.RawD + var doc bson.D err = bson.Unmarshal(raw, &doc) if err != nil { return nil, fmt.Errorf("cannot reunmarshal nested applyOps op: %s", err) diff --git a/src/mongo/gotools/mongorestore/oplog_test.go b/src/mongo/gotools/mongorestore/oplog_test.go index fda9ebcc1c9..cfb52bdbf0a 100644 --- a/src/mongo/gotools/mongorestore/oplog_test.go +++ b/src/mongo/gotools/mongorestore/oplog_test.go @@ -131,6 +131,17 @@ func TestOplogRestore(t *testing.T) { t.Fatalf("No server available") } + session, err := testutil.GetBareSession() + if err != nil { + t.Fatalf("No server available") + } + defer session.Close() + fcv := testutil.GetFCV(session) + var shouldPreserveUUID bool + if cmp, err := testutil.CompareFCV(fcv, "3.6"); err != nil || cmp >= 0 { + shouldPreserveUUID = true + } + Convey("With a test MongoRestore", t, func() { inputOptions := &InputOptions{ Directory: "testdata/oplogdump", @@ -140,6 +151,7 @@ func TestOplogRestore(t *testing.T) { NumParallelCollections: 1, NumInsertionWorkers: 1, Drop: true, + PreserveUUID: shouldPreserveUUID, } nsOptions := &NSOptions{} provider, toolOpts, err := testutil.GetBareSessionProvider() @@ -170,3 +182,41 @@ func TestOplogRestore(t *testing.T) { So(count, ShouldEqual, 10) }) } + +func TestOplogRestoreTools2002(t *testing.T) { + testutil.VerifyTestType(t, testutil.IntegrationTestType) + _, err := testutil.GetBareSession() + if err != nil { + t.Fatalf("No server available") + } + + Convey("With a test MongoRestore", t, func() { + inputOptions := &InputOptions{ + Directory: "testdata/tools-2002", + OplogReplay: true, + } + outputOptions := &OutputOptions{ + NumParallelCollections: 1, + NumInsertionWorkers: 1, + Drop: true, + } + nsOptions := &NSOptions{} + provider, toolOpts, err := testutil.GetBareSessionProvider() + if err != nil { + log.Logvf(log.Always, "error connecting to host: %v", err) + os.Exit(util.ExitError) + } + restore := MongoRestore{ + ToolOptions: toolOpts, + OutputOptions: outputOptions, + InputOptions: inputOptions, + NSOptions: nsOptions, + SessionProvider: provider, + TargetDirectory: inputOptions.Directory, + } + + // Run mongorestore + err = restore.Restore() + So(err, ShouldBeNil) + }) +} diff --git a/src/mongo/gotools/mongorestore/options.go b/src/mongo/gotools/mongorestore/options.go index 20f57080042..4c2c28463a8 100644 --- a/src/mongo/gotools/mongorestore/options.go +++ b/src/mongo/gotools/mongorestore/options.go @@ -49,6 +49,7 @@ type OutputOptions struct { NumInsertionWorkers int `long:"numInsertionWorkersPerCollection" description:"number of insert operations to run concurrently per collection (1 by default)" default:"1" default-mask:"-"` StopOnError bool `long:"stopOnError" description:"stop restoring if an error is encountered on insert (off by default)"` BypassDocumentValidation bool `long:"bypassDocumentValidation" description:"bypass document validation"` + PreserveUUID bool `long:"preserveUUID" description:"preserve original collection UUIDs (off by default, requires drop)"` TempUsersColl string `long:"tempUsersColl" default:"tempusers" hidden:"true"` TempRolesColl string `long:"tempRolesColl" default:"temproles" hidden:"true"` BulkBufferSize int `long:"batchSize" default:"1000" hidden:"true"` diff --git a/src/mongo/gotools/mongorestore/restore.go b/src/mongo/gotools/mongorestore/restore.go index fda9c16f9a0..a0175807d37 100644 --- a/src/mongo/gotools/mongorestore/restore.go +++ b/src/mongo/gotools/mongorestore/restore.go @@ -117,6 +117,7 @@ func (restore *MongoRestore) RestoreIntent(intent *intents.Intent) error { var options bson.D var indexes []IndexDocument + var uuid string // get indexes from system.indexes dump if we have it but don't have metadata files if intent.MetadataFile == nil { @@ -138,14 +139,24 @@ func (restore *MongoRestore) RestoreIntent(intent *intents.Intent) error { defer intent.MetadataFile.Close() log.Logvf(log.Always, "reading metadata for %v from %v", intent.Namespace(), intent.MetadataLocation) - metadata, err := ioutil.ReadAll(intent.MetadataFile) + metadataJSON, err := ioutil.ReadAll(intent.MetadataFile) if err != nil { return fmt.Errorf("error reading metadata from %v: %v", intent.MetadataLocation, err) } - options, indexes, err = restore.MetadataFromJSON(metadata) + metadata, err := restore.MetadataFromJSON(metadataJSON) if err != nil { return fmt.Errorf("error parsing metadata from %v: %v", intent.MetadataLocation, err) } + if metadata != nil { + options = metadata.Options + indexes = metadata.Indexes + if restore.OutputOptions.PreserveUUID { + if metadata.UUID == "" { + return fmt.Errorf("--preserveUUID used but no UUID found in %v", intent.MetadataLocation) + } + uuid = metadata.UUID + } + } // The only way to specify options on the idIndex is at collection creation time. // This loop pulls out the idIndex from `indexes` and sets it in `options`. @@ -153,7 +164,9 @@ func (restore *MongoRestore) RestoreIntent(intent *intents.Intent) error { // The index with the name "_id_" will always be the idIndex. if index.Options["name"].(string) == "_id_" { // Remove the index version (to use the default) unless otherwise specified. - if !restore.OutputOptions.KeepIndexVersion { + // If preserving UUID, we have to create a collection via + // applyops, which requires the "v" key. + if !restore.OutputOptions.KeepIndexVersion && !restore.OutputOptions.PreserveUUID { delete(index.Options, "v") } index.Options["ns"] = intent.Namespace() @@ -180,7 +193,7 @@ func (restore *MongoRestore) RestoreIntent(intent *intents.Intent) error { if !collectionExists { log.Logvf(log.Info, "creating collection %v %s", intent.Namespace(), logMessageSuffix) log.Logvf(log.DebugHigh, "using collection options: %#v", options) - err = restore.CreateCollection(intent, options) + err = restore.CreateCollection(intent, options, uuid) if err != nil { return fmt.Errorf("error creating collection %v: %v", intent.Namespace(), err) } |