mongo-oplog-backup 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ee599dbf9fcfddbf310a1f8196d04e4a9aea58f3
4
- data.tar.gz: 60ee3f338862e1a4eecb4aa3c272acb110efc757
3
+ metadata.gz: 4c98c007c62b80bc6c87b1f141deba9d27933865
4
+ data.tar.gz: 9a731f45965d4218bbef98e711908d236d41d29c
5
5
  SHA512:
6
- metadata.gz: 5ddbe8b87d6ef34fdb5b5d0bda352fd41c4a5b6f11a5588d00c9c53e7325d433d40d3dfd3325ae1f03a1f8290039357a8eebbe0a09572131dc04f0daba7c7f92
7
- data.tar.gz: 0ad3c9e703de40f772f137c8c7525fb6e049b78d170e3d8ddee2900f7e31f1422e765e4ae8bb94f6e13922fe586fe0f987b4a26ed2820849c654b8e0e480f28e
6
+ metadata.gz: 5642b1a2790b9d2493c1d5e7f90e8baae53bc6d9c6a81084475d03100903b6cb704124f7a4336060f7c9256900db00a677b06c548c665f5ccdcbae7989f28bea
7
+ data.tar.gz: cbba467fb5f8c9ffe9c30a79298c6d3779d5e590ebab20bf874139b76b1adb288560caf7910f734093f0dfe5ce5e563a041c8b7ce5bc4af0f75ae8f8d3a3aef6
@@ -18,6 +18,9 @@ opts = Slop.parse(help: true, strict: true) do
18
18
  on :ssl, "Connect to a mongod instance over an SSL connection"
19
19
  on :sslAllowInvalidCertificates, "Allow connections to a mongod instance with an invalid certificate"
20
20
  on :sslCAFile, "Specifies a Certificate Authority file for validating the SSL certificate provided by the mongod instance.", argument: :required
21
+ on :sslPEMKeyFile, "Specifies a SSL client certificate and private key for authenticating with the mongod instance.", argument: :required
22
+ on :sslPEMKeyPassword, "Specifies the password used to open the sslPEMKeyFile, if required.", argument: :required
23
+ on :authenticationDatabase, "Specifies the database to authenticate against. This can be used to override the default selection.", argument: :required
21
24
  on :host, "Specifies a resolvable hostname for the mongod that you wish to backup.", default: 'localhost', argument: :required
22
25
  on :port, "Specifies the port that mongod is running on", default: '27017', argument: :required
23
26
  on :u, :username, "Specifies a username to authenticate to the MongoDB instance, if your database requires authentication. Use in conjunction with the --password option to supply a password.", argument: :required
@@ -37,6 +40,9 @@ opts = Slop.parse(help: true, strict: true) do
37
40
  config_opts[:username] = opts[:username]
38
41
  config_opts[:password] = opts[:password]
39
42
  config_opts[:sslCAFile] = opts[:sslCAFile]
43
+ config_opts[:sslPEMKeyFile] = opts[:sslPEMKeyFile]
44
+ config_opts[:sslPEMKeyPassword] = opts[:sslPEMKeyPassword]
45
+ config_opts[:authenticationDatabase] = opts[:authenticationDatabase]
40
46
 
41
47
  mode = :auto
42
48
  if opts.full?
@@ -66,4 +72,60 @@ opts = Slop.parse(help: true, strict: true) do
66
72
  puts "mongorestore [--drop] [--gzip] --oplogReplay #{File.join(dir, 'dump')}"
67
73
  end
68
74
  end
75
+
76
+ command 'restore' do
77
+ banner 'Usage: mongo-oplog-backup restore [options]'
78
+ on :v, :verbose, 'Enable verbose mode'
79
+ on :d, :dir, "Directory containing the backup to restore. Must contain a 'dump' folder.", argument: :required
80
+ on :full, 'Do a full restore, followed by an oplog restore.'
81
+ on :oplog, 'Restore only the oplogs. Assumes a manual restore has already been done.'
82
+
83
+ on :gzip, "Use gzip compression. This option is ignored by Oplog mode, because it autodetects."
84
+ on :ssl, "Connect to a mongod instance over an SSL connection"
85
+ on :sslAllowInvalidCertificates, "Allow connections to a mongod instance with an invalid certificate"
86
+ on :sslCAFile, "Specifies a Certificate Authority file for validating the SSL certificate provided by the mongod instance.", argument: :required
87
+ on :host, "Specifies a resolvable hostname for the mongod that you wish to backup.", default: 'localhost', argument: :required
88
+ on :port, "Specifies the port that mongod is running on", default: '27017', argument: :required
89
+ on :u, :username, "Specifies a username to authenticate to the MongoDB instance, if your database requires authentication. Use in conjunction with the --password option to supply a password.", argument: :required
90
+ on :p, :password, "Specifies a password to authenticate to the MongoDB instance. Use in conjunction with the --username option to supply a username. Note. the password will not be prompted for, so must be passed as an argument", argument: :required
91
+
92
+ on :noIndexRestore, "Don't restore indexes."
93
+ on :oplogLimit, "Only include oplog entries before the provided Timestamp: <seconds>[:ordinal]", argument: :required
94
+ on :oplogStartAt, "Ignore oplog batches before the provided Timestamp: <seconds>[:ordinal]. This is useful for resuming a restore after an error.", argument: :required
95
+
96
+ run do |opts, args|
97
+ dir = opts[:dir]
98
+ raise ArgumentError, 'dir must be specified' unless dir
99
+ raise ArgumentError, 'dir must contain a dump subfolder' unless File.directory?(File.join(dir, 'dump'))
100
+ config_opts = {
101
+ dir: dir,
102
+ ssl: opts.ssl?,
103
+ gzip: opts.gzip?,
104
+ sslAllowInvalidCertificates: opts.sslAllowInvalidCertificates?,
105
+ noIndexRestore: opts.noIndexRestore?
106
+ }
107
+ config_opts[:host] = opts[:host]
108
+ config_opts[:port] = opts[:port]
109
+ config_opts[:username] = opts[:username]
110
+ config_opts[:password] = opts[:password]
111
+ config_opts[:sslCAFile] = opts[:sslCAFile]
112
+ config_opts[:oplogLimit] = opts[:oplogLimit]
113
+ config_opts[:oplogStartAt] = opts[:oplogStartAt]
114
+
115
+ if opts.full?
116
+ mode = :full
117
+ elsif opts.oplog?
118
+ mode = :oplog
119
+ else
120
+ raise ArgumentError, "either full or oplog must be specified"
121
+ end
122
+
123
+ restore_opts = {}
124
+ restore_opts[:oplogLimit] = opts[:oplogLimit]
125
+
126
+ config = MongoOplogBackup::Config.new(config_opts)
127
+ restore = MongoOplogBackup::Restore.new(config)
128
+ restore.perform(mode, restore_opts)
129
+ end
130
+ end
69
131
  end
@@ -8,6 +8,7 @@ require 'mongo_oplog_backup/command'
8
8
  require 'mongo_oplog_backup/config'
9
9
  require 'mongo_oplog_backup/backup'
10
10
  require 'mongo_oplog_backup/oplog'
11
+ require 'mongo_oplog_backup/restore'
11
12
 
12
13
  module MongoOplogBackup
13
14
  def self.log
@@ -18,6 +18,9 @@ module MongoOplogBackup
18
18
  options[:ssl] = conf["ssl"] unless conf["ssl"].nil?
19
19
  options[:sslAllowInvalidCertificates] = conf["sslAllowInvalidCertificates"] unless conf["sslAllowInvalidCertificates"].nil?
20
20
  options[:sslCAFile] = conf["sslCAFile"] unless conf["sslCAFile"].nil?
21
+ options[:sslPEMKeyFile] = conf["sslPEMKeyFile"] unless conf["sslPEMKeyFile"].nil?
22
+ options[:sslPEMKeyPassword] = conf["sslPEMKeyPassword"] unless conf["sslPEMKeyPassword"].nil?
23
+ options[:authenticationDatabase] = conf["authenticationDatabase"] unless conf["authenticationDatabase"].nil?
21
24
  options[:host] = conf["host"] unless conf["host"].nil?
22
25
  options[:port] = conf["port"].to_s unless conf["port"].nil?
23
26
  options[:username] = conf["username"] unless conf["username"].nil?
@@ -39,11 +42,18 @@ module MongoOplogBackup
39
42
  args = []
40
43
  args << '--ssl' if options[:ssl]
41
44
  args << '--sslAllowInvalidCertificates' if options[:sslAllowInvalidCertificates]
42
- [:host, :port, :username, :password, :sslCAFile].each do |option|
45
+ [:host, :port, :username, :password, :sslCAFile, :sslPEMKeyFile, :sslPEMKeyPassword].each do |option|
43
46
  args += ["--#{option}", options[option].strip] if options[option]
44
47
  end
48
+
49
+ if options[:authenticationDatabase]
50
+ args += ['--authenticationDatabase', options[:authenticationDatabase]]
51
+ else
52
+ args += ['--authenticationDatabase', 'admin'] if options[:username] && !options[:sslPEMKeyFile]
53
+ args += ['--authenticationDatabase', '$external'] if options[:sslPEMKeyFile]
54
+ end
45
55
 
46
- args += ['--authenticationDatabase', 'admin'] if options[:username]
56
+ args += ['--authenticationMechanism', 'MONGODB-X509'] if options[:sslPEMKeyFile]
47
57
 
48
58
  args
49
59
  end
@@ -78,7 +88,11 @@ module MongoOplogBackup
78
88
  end
79
89
 
80
90
  def mongo(db, script)
81
- exec(['mongo'] + command_line_options + ['--quiet', '--norc', script])
91
+ exec(['mongo'] + command_line_options + ['--quiet', '--norc', db, script])
92
+ end
93
+
94
+ def mongorestore(*args)
95
+ exec(['mongorestore'] + command_line_options + args.flatten)
82
96
  end
83
97
 
84
98
  def command_string(cmd)
@@ -21,10 +21,20 @@ module MongoOplogBackup::Ext
21
21
  end
22
22
 
23
23
  module ClassMethods
24
- # Accepts {'t' => seconds, 'i' => increment}
24
+ # Accepts {'t' => seconds, 'i' => increment} or {'$timestamp' => {'t' => seconds, 'i' => increment}}
25
25
  def from_json(data)
26
+ data = data['$timestamp'] if data['$timestamp']
26
27
  self.new(data['t'], data['i'])
27
28
  end
29
+
30
+ # Accepts: <seconds>[:ordinal]
31
+ def from_string(string)
32
+ match = /(\d+)(?::(\d+))?/.match(string)
33
+ return nil unless match
34
+ s1 = match[1].to_i
35
+ i1 = match[2].to_i
36
+ self.new(s1,i1)
37
+ end
28
38
  end
29
39
 
30
40
  end
@@ -0,0 +1,141 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ require 'mongo_oplog_backup/oplog'
4
+
5
+ module MongoOplogBackup
6
+ class Restore
7
+ attr_reader :config
8
+
9
+ def backup_folder
10
+ File.join(config.backup_dir)
11
+ end
12
+
13
+ def oplog_restore_folder
14
+ File.join(backup_folder, 'tmp-restore')
15
+ end
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+
21
+ # Given a directory of oplog dumps generated by the backup feature,
22
+ # iteratively mongorestore them.
23
+ # Mongorestore <3.4 expects a file named oplog.bson the directory specified
24
+ # Mongorestore 3.4 adds support for --oplogFile parameter, which simplifies things.
25
+ def restore_oplogs(dir, options={})
26
+ default_restore_args = ['--stopOnError', '--oplogReplay']
27
+ default_restore_args << '--noIndexRestore' if !!config.options[:noIndexRestore]
28
+ if options[:oplogLimit]
29
+ default_restore_args += ['--oplogLimit', options[:oplogLimit]]
30
+ oplog_limit = BSON::Timestamp.from_string(options[:oplogLimit])
31
+ end
32
+
33
+ oplog_start_at = BSON::Timestamp.from_string(config.options[:oplogStartAt]) if config.options[:oplogStartAt]
34
+
35
+ source_files = Oplog.find_oplogs(dir)
36
+
37
+ validate_continuity!(source_files)
38
+
39
+ MongoOplogBackup.log.debug "Starting oplog restore..."
40
+ source_files.each do |filename|
41
+ # TODO: mongorestore 3.4 supports --oplogFile
42
+
43
+ unless oplog_start_at.nil?
44
+ timestamps = Oplog.timestamps_from_filename(filename)
45
+ if timestamps[:last] <= oplog_start_at
46
+ MongoOplogBackup.log.debug "Skipping batch: #{filename}. Last op in batch (ts: #{timestamps[:last]}) is before oplogStartAt: #{oplog_start_at}"
47
+ next
48
+ end
49
+ end
50
+ unless oplog_limit.nil?
51
+ timestamps = Oplog.timestamps_from_filename(filename)
52
+ if timestamps[:first] > oplog_limit
53
+ MongoOplogBackup.log.debug "Skipping batch: #{filename}. First op in batch (ts: #{timestamps[:first]}) is after oplogLimit: #{oplog_limit}"
54
+ next
55
+ end
56
+ end
57
+
58
+ temp_file_path = create_temp_oplog_dir(dir, filename)
59
+ oplog_dir_path = File.dirname(temp_file_path)
60
+
61
+ restore_args = default_restore_args.dup
62
+ restore_args << '--gzip' if Oplog.gzip_fingerprint(filename)
63
+ restore_args << oplog_dir_path
64
+ status = config.mongorestore(restore_args).status
65
+ if status != 0
66
+ MongoOplogBackup.log.error("Mongorestore failed during oplog restore. Aborting. Exit code: #{status}")
67
+ raise 'Oplog restore failed.'
68
+ end
69
+
70
+ begin
71
+ File.unlink(temp_file_path)
72
+ Dir.rmdir(oplog_dir_path)
73
+ rescue SystemCallError => e
74
+ MongoOplogBackup.log.error("Clean-up error for '#{temp_file_path}. #{e.message}")
75
+ raise
76
+ end
77
+ end
78
+ MongoOplogBackup.log.debug "Oplog restore complete."
79
+ end
80
+
81
+ def restore_dump(dir, options={})
82
+ restore_args = ['--stopOnError']
83
+ restore_args << '--noIndexRestore' if !!config.options[:noIndexRestore]
84
+ restore_args << '--gzip' if config.use_compression?
85
+ restore_args << File.join(dir, 'dump')
86
+
87
+ MongoOplogBackup.log.debug "Starting full restore..."
88
+ status = config.mongorestore(restore_args).status
89
+ if status != 0
90
+ MongoOplogBackup.log.error("Mongorestore failed.")
91
+ raise 'Full restore failed.'
92
+ end
93
+ MongoOplogBackup.log.debug "Full restore complete."
94
+ end
95
+
96
+ def perform(mode, options={})
97
+ if options[:oplogLimit]
98
+ raise ArgumentError, "oplogLimit is not a timestamp: eg. <seconds>[:ordinal]" unless options[:oplogLimit] =~ /\A\d+(?::\d+)?\z/
99
+ end
100
+
101
+ if mode == :oplog
102
+ restore_oplogs(backup_folder, options)
103
+ elsif mode == :full
104
+ restore_dump(backup_folder, options)
105
+ restore_oplogs(backup_folder, options)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # Check for gaps in timestamps based only on the filename, trusting that the oplog set is complete (ie. the filename timestamps match the actual data)
112
+ def validate_continuity!(source_files)
113
+ expected_first = nil
114
+ MongoOplogBackup.log.debug "Checking timestamps..."
115
+ source_files.each do |filename|
116
+ timestamps = Oplog.timestamps_from_filename(filename)
117
+ raise("Filename without timestamps found in restore set: #{filename}") if timestamps.nil?
118
+ if expected_first && expected_first != timestamps[:first]
119
+ raise "Missing oplog dump? Expected a filename with first timestamp '#{expected_first}', but got '#{timestamps[:first]}' from filename '#{filename}'."
120
+ end
121
+ expected_first = timestamps[:last]
122
+ end
123
+ true
124
+ end
125
+
126
+ def create_temp_oplog_dir dir, filename
127
+ temp_dir = File.join(dir, 'tmp-restore', File.basename(filename))
128
+ temp_file_path = File.join(temp_dir, 'oplog.bson')
129
+ FileUtils.mkdir_p(temp_dir)
130
+ File.link(filename, temp_file_path)
131
+ return temp_file_path
132
+ rescue Errno::EEXIST => e
133
+ # Probably, 'new_name' already exists when creating link.
134
+ MongoOplogBackup.log.warn("Temporary oplog.bson link already exists: #{e.message}. Assuming valid.")
135
+ return temp_file_path
136
+ rescue SystemCallError => e
137
+ MongoOplogBackup.log.error("Setup error for oplog '#{filename}' in '#{temp_file_path}")
138
+ raise
139
+ end
140
+ end
141
+ end
@@ -1,3 +1,3 @@
1
1
  module MongoOplogBackup
2
- VERSION = "0.0.11"
2
+ VERSION = "0.0.12"
3
3
  end
@@ -41,6 +41,15 @@ describe MongoOplogBackup::Ext::Timestamp do
41
41
  ts.as_json.should == json
42
42
  end
43
43
 
44
+ it 'should handle extended-json timestamp format' do
45
+ json = { "t" => 1408004593, "i" => 20}
46
+ extendedjson = {"$timestamp" => json}
47
+ ts = BSON::Timestamp.from_json(extendedjson)
48
+ ts.seconds.should == 1408004593
49
+ ts.increment.should == 20
50
+ ts.as_json.should == json
51
+ end
52
+
44
53
  it 'should define to_s' do
45
54
  ts = BSON::Timestamp.new(1408004593, 2)
46
55
  ts.to_s.should == '1408004593:2'
@@ -0,0 +1,57 @@
1
+ #!/bin/bash -ex
2
+
3
+ echo "This requires a mongodb instance running on 27017."
4
+ echo "It will first SHUTDOWN any instances already running on that port, and then start new ones."
5
+ echo "If this is undesired, CTRL+C within 5 seconds..."
6
+ sleep 5
7
+ echo "Here we go..."
8
+ mongo --port 27017 admin --eval 'db.shutdownServer({force: true})' || true
9
+ sleep 5
10
+ rm -rf backup-test/*
11
+ rm -rf testdb
12
+ mkdir testdb
13
+ mongod --port 27017 --dbpath testdb --replSet rs0 --oplogSize 20 --noprealloc --fork --smallfiles --logpath mongodb.log
14
+ sleep 3
15
+ mongo --port 27017 admin --eval 'printjson(rs.initiate());'
16
+ sleep 20
17
+
18
+
19
+
20
+ bundle exec rspec
21
+
22
+
23
+
24
+ bundle exec bin/mongo-oplog-backup backup --port 27017 --dir backup-test/ --gzip --full
25
+ mongo --port 27017 backup-test --eval 'db.test.insert({"a":2})'
26
+
27
+ bundle exec bin/mongo-oplog-backup backup --port 27017 --dir backup-test/ --gzip --oplog
28
+
29
+ sleep 5
30
+ mongo --port 27017 backup-test --eval 'db.test.insert({"a":3})'
31
+ bundle exec bin/mongo-oplog-backup backup --port 27017 --dir backup-test/ --oplog
32
+
33
+
34
+ sleep 5
35
+ mongo --port 27017 backup-test --eval 'db.test.insert({"a":4})'
36
+ bundle exec bin/mongo-oplog-backup backup --port 27017 --dir backup-test/ --oplog
37
+
38
+
39
+
40
+ mongo --port 27017 admin --eval 'db.shutdownServer({force: true})'
41
+ sleep 5
42
+ rm -rf testdb/*
43
+ mongod --port 27017 --dbpath testdb --replSet rs0 --oplogSize 20 --noprealloc --fork --smallfiles --logpath mongodb.log
44
+ sleep 3
45
+ mongo --port 27017 admin --eval 'printjson(rs.initiate());'
46
+ sleep 20
47
+
48
+ export BACKUPDIR=`ls -1t backup-test/ |grep backup- |head -n 1`
49
+
50
+ bundle exec bin/mongo-oplog-backup restore --full --gzip --dir backup-test/$BACKUPDIR --port 27017
51
+ mongo --port 27017 backup-test --eval 'db.test.find()'
52
+
53
+
54
+ #mongorestore --gzip --port 27017 backup-test/$BACKUPDIR/dump
55
+ #mongo --port 27017 backup-test --eval 'db.test.find()'
56
+ #bundle exec bin/mongo-oplog-backup restore --oplog --dir backup-test/$BACKUPDIR --port 27017
57
+ #mongo --port 27017 backup-test --eval 'db.test.find()'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo-oplog-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ralf Kistner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-25 00:00:00.000000000 Z
11
+ date: 2017-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bson
@@ -118,6 +118,7 @@ files:
118
118
  - lib/mongo_oplog_backup/ext/enumerable.rb
119
119
  - lib/mongo_oplog_backup/ext/timestamp.rb
120
120
  - lib/mongo_oplog_backup/oplog.rb
121
+ - lib/mongo_oplog_backup/restore.rb
121
122
  - lib/mongo_oplog_backup/version.rb
122
123
  - mongo-oplog-backup.gemspec
123
124
  - oplog-last-timestamp.js
@@ -136,6 +137,7 @@ files:
136
137
  - spec/oplog_spec.rb
137
138
  - spec/spec_helper.rb
138
139
  - spec/timestamp_spec.rb
140
+ - testing.sh
139
141
  homepage: ''
140
142
  licenses:
141
143
  - MIT