digitalbits-core-backup 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +58 -0
  3. data/Gemfile +2 -0
  4. data/LICENSE.txt +202 -0
  5. data/README.md +93 -0
  6. data/Rakefile +9 -0
  7. data/bin/digitalbits-core-backup +46 -0
  8. data/config/sample.yaml +8 -0
  9. data/digitalbits-core-backup.gemspec +26 -0
  10. data/lib/digitalbits-core-backup/cmd.rb +28 -0
  11. data/lib/digitalbits-core-backup/cmd.rb-e +28 -0
  12. data/lib/digitalbits-core-backup/cmd_result.rb +14 -0
  13. data/lib/digitalbits-core-backup/cmd_result.rb-e +14 -0
  14. data/lib/digitalbits-core-backup/config.rb +37 -0
  15. data/lib/digitalbits-core-backup/config.rb-e +37 -0
  16. data/lib/digitalbits-core-backup/database.rb +67 -0
  17. data/lib/digitalbits-core-backup/database.rb-e +67 -0
  18. data/lib/digitalbits-core-backup/filesystem.rb +62 -0
  19. data/lib/digitalbits-core-backup/filesystem.rb-e +62 -0
  20. data/lib/digitalbits-core-backup/job.rb +223 -0
  21. data/lib/digitalbits-core-backup/job.rb-e +223 -0
  22. data/lib/digitalbits-core-backup/restore/database.rb +36 -0
  23. data/lib/digitalbits-core-backup/restore/database.rb-e +36 -0
  24. data/lib/digitalbits-core-backup/restore/filesystem.rb +36 -0
  25. data/lib/digitalbits-core-backup/restore/filesystem.rb-e +36 -0
  26. data/lib/digitalbits-core-backup/s3.rb +64 -0
  27. data/lib/digitalbits-core-backup/s3.rb-e +64 -0
  28. data/lib/digitalbits-core-backup/tar.rb +37 -0
  29. data/lib/digitalbits-core-backup/tar.rb-e +37 -0
  30. data/lib/digitalbits-core-backup/utils.rb +197 -0
  31. data/lib/digitalbits-core-backup/utils.rb-e +197 -0
  32. data/lib/digitalbits-core-backup/version.rb +3 -0
  33. data/lib/digitalbits-core-backup/version.rb-e +3 -0
  34. data/lib/digitalbits-core-backup.rb +21 -0
  35. metadata +147 -0
@@ -0,0 +1,67 @@
1
+ require 'pg'
2
+
3
+ module DigitalbitsCoreBackup
4
+ class Database
5
+ include Contracts
6
+
7
+ attr_reader :dbname
8
+
9
+ Contract DigitalbitsCoreBackup::Config => Contracts::Any
10
+ def initialize(config)
11
+ @config = config
12
+ @working_dir = DigitalbitsCoreBackup::Utils.create_working_dir(@config.get('working_dir'))
13
+ @dbname = check_db_connection
14
+ end
15
+
16
+ public
17
+ def backup()
18
+ pg_dump()
19
+ end
20
+
21
+ private
22
+ #Contract nil, DigitalbitsCoreBackup::CmdResult
23
+ def pg_dump()
24
+ cmd = DigitalbitsCoreBackup::Cmd.new(@working_dir)
25
+ puts "info: connecting (#{get_db_details(@config.get('core_config')).gsub(/password=(.*)/, 'password=********')})"
26
+ pg_dump = cmd.run('pg_dump', ['--dbname', @dbname, '--format', 'd', '--jobs', "#{DigitalbitsCoreBackup::Utils.num_cores?()}", '--file', "#{@working_dir}/core-db/"])
27
+ if pg_dump.success then
28
+ puts "info: database backup complete"
29
+ end
30
+ end
31
+
32
+ private
33
+ # check we have passwordless access to postgres
34
+ Contract nil => Contracts::Any
35
+ def check_db_connection()
36
+ # TODO move to config
37
+ dbname = get_db_details(@config.get('core_config'))
38
+ begin
39
+ conn = PG.connect(dbname)
40
+ conn.exec("SELECT * FROM pg_stat_activity") do |result|
41
+ # 2 => PGRES_TUPLES_OK
42
+ if result.result_status == 2 then
43
+ return dbname
44
+ end
45
+ end
46
+ rescue PG::Error
47
+ puts "error: failed to connect to db"
48
+ exit
49
+ end
50
+ end
51
+
52
+ private
53
+ Contract String => String
54
+ def get_db_details(config)
55
+ File.open(config,'r') do |fd|
56
+ fd.each_line do |line|
57
+ if (line[/^DATABASE=/]) then
58
+ # "postgresql://dbname=digitalbits user=digitalbits"
59
+ connection_str = /^DATABASE=(.*)/.match(line).captures[0].gsub!(/"postgresql:\/\/(.*)"$/,'\1')
60
+ return connection_str
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,67 @@
1
+ require 'pg'
2
+
3
+ module DigitalbitsCoreBackup
4
+ class Database
5
+ include Contracts
6
+
7
+ attr_reader :dbname
8
+
9
+ Contract DigitalbitsCoreBackup::Config => Contracts::Any
10
+ def initialize(config)
11
+ @config = config
12
+ @working_dir = DigitalbitsCoreBackup::Utils.create_working_dir(@config.get('working_dir'))
13
+ @dbname = check_db_connection
14
+ end
15
+
16
+ public
17
+ def backup()
18
+ pg_dump()
19
+ end
20
+
21
+ private
22
+ #Contract nil, DigitalbitsCoreBackup::CmdResult
23
+ def pg_dump()
24
+ cmd = DigitalbitsCoreBackup::Cmd.new(@working_dir)
25
+ puts "info: connecting (#{get_db_details(@config.get('core_config')).gsub(/password=(.*)/, 'password=********')})"
26
+ pg_dump = cmd.run('pg_dump', ['--dbname', @dbname, '--format', 'd', '--jobs', "#{DigitalbitsCoreBackup::Utils.num_cores?()}", '--file', "#{@working_dir}/core-db/"])
27
+ if pg_dump.success then
28
+ puts "info: database backup complete"
29
+ end
30
+ end
31
+
32
+ private
33
+ # check we have passwordless access to postgres
34
+ Contract nil => Contracts::Any
35
+ def check_db_connection()
36
+ # TODO move to config
37
+ dbname = get_db_details(@config.get('core_config'))
38
+ begin
39
+ conn = PG.connect(dbname)
40
+ conn.exec("SELECT * FROM pg_stat_activity") do |result|
41
+ # 2 => PGRES_TUPLES_OK
42
+ if result.result_status == 2 then
43
+ return dbname
44
+ end
45
+ end
46
+ rescue PG::Error
47
+ puts "error: failed to connect to db"
48
+ exit
49
+ end
50
+ end
51
+
52
+ private
53
+ Contract String => String
54
+ def get_db_details(config)
55
+ File.open(config,'r') do |fd|
56
+ fd.each_line do |line|
57
+ if (line[/^DATABASE=/]) then
58
+ # "postgresql://dbname=digitalbits user=digitalbits"
59
+ connection_str = /^DATABASE=(.*)/.match(line).captures[0].gsub!(/"postgresql:\/\/(.*)"$/,'\1')
60
+ return connection_str
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,62 @@
1
+ module DigitalbitsCoreBackup
2
+ class Filesystem
3
+ include Contracts
4
+
5
+ class ReadError < StandardError ; end
6
+
7
+ attr_reader :core_data_dir
8
+
9
+ Contract DigitalbitsCoreBackup::Config => Contracts::Any
10
+ def initialize(config)
11
+ @config = config
12
+ @working_dir = DigitalbitsCoreBackup::Utils.create_working_dir(@config.get('working_dir'))
13
+ @core_data_dir = get_core_data_dir(@config.get('core_config'))
14
+ end
15
+
16
+ public
17
+ def backup()
18
+ create_core_data_tar()
19
+ end
20
+
21
+ private
22
+ #Contract nil, DigitalbitsCoreBackup::CmdResult
23
+ def create_core_data_tar()
24
+ if core_data_readable?() then
25
+ # create the tar balls
26
+ puts "info: creating filesystem backup"
27
+ Dir.chdir(@core_data_dir)
28
+ DigitalbitsCoreBackup::Tar.pack("#{@working_dir}/core-fs.tar", '.')
29
+ Dir.chdir(@working_dir)
30
+ end
31
+ end
32
+
33
+ private
34
+ # TODO: replace with Utils.readable ?
35
+ # check we have read access to the digitalbits-core data
36
+ Contract nil => Contracts::Any
37
+ def core_data_readable?()
38
+ if File.readable?("#{@core_data_dir}/digitalbits-core.lock") then
39
+ puts "info: processing #{@core_data_dir}"
40
+ return true
41
+ else
42
+ puts "error: can not access #{@core_data_dir}"
43
+ raise ReadError
44
+ end
45
+ end
46
+
47
+ private
48
+ Contract String => String
49
+ def get_core_data_dir(config)
50
+ File.open(config,'r') do |fd|
51
+ fd.each_line do |line|
52
+ if (line[/^BUCKET_DIR_PATH=/]) then
53
+ # "postgresql://dbname=digitalbits user=digitalbits"
54
+ core_data_dir = /^BUCKET_DIR_PATH="(.*)"$/.match(line).captures[0]
55
+ return core_data_dir
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ module DigitalbitsCoreBackup
2
+ class Filesystem
3
+ include Contracts
4
+
5
+ class ReadError < StandardError ; end
6
+
7
+ attr_reader :core_data_dir
8
+
9
+ Contract DigitalbitsCoreBackup::Config => Contracts::Any
10
+ def initialize(config)
11
+ @config = config
12
+ @working_dir = DigitalbitsCoreBackup::Utils.create_working_dir(@config.get('working_dir'))
13
+ @core_data_dir = get_core_data_dir(@config.get('core_config'))
14
+ end
15
+
16
+ public
17
+ def backup()
18
+ create_core_data_tar()
19
+ end
20
+
21
+ private
22
+ #Contract nil, DigitalbitsCoreBackup::CmdResult
23
+ def create_core_data_tar()
24
+ if core_data_readable?() then
25
+ # create the tar balls
26
+ puts "info: creating filesystem backup"
27
+ Dir.chdir(@core_data_dir)
28
+ DigitalbitsCoreBackup::Tar.pack("#{@working_dir}/core-fs.tar", '.')
29
+ Dir.chdir(@working_dir)
30
+ end
31
+ end
32
+
33
+ private
34
+ # TODO: replace with Utils.readable ?
35
+ # check we have read access to the digitalbits-core data
36
+ Contract nil => Contracts::Any
37
+ def core_data_readable?()
38
+ if File.readable?("#{@core_data_dir}/digitalbits-core.lock") then
39
+ puts "info: processing #{@core_data_dir}"
40
+ return true
41
+ else
42
+ puts "error: can not access #{@core_data_dir}"
43
+ raise ReadError
44
+ end
45
+ end
46
+
47
+ private
48
+ Contract String => String
49
+ def get_core_data_dir(config)
50
+ File.open(config,'r') do |fd|
51
+ fd.each_line do |line|
52
+ if (line[/^BUCKET_DIR_PATH=/]) then
53
+ # "postgresql://dbname=digitalbits user=digitalbits"
54
+ core_data_dir = /^BUCKET_DIR_PATH="(.*)"$/.match(line).captures[0]
55
+ return core_data_dir
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,223 @@
1
+ module DigitalbitsCoreBackup
2
+ class Job
3
+
4
+ class NoConfig < StandardError ; end
5
+
6
+ def initialize(**args)
7
+ if args.has_key?(:config) then
8
+ @config = DigitalbitsCoreBackup::Config.new(args[:config])
9
+ else
10
+ puts "info: no config provided"
11
+ raise NoConfig
12
+ end
13
+
14
+ # Set run time options
15
+ @verify = args[:verify] if args.has_key?(:verify)
16
+ @clean = args[:clean] if args.has_key?(:clean)
17
+ @listlen = args[:listlen]
18
+
19
+ # Set common run time parameters
20
+ @job_type = args[:type]
21
+ @gpg_key = @config.get('gpg_key')
22
+ @working_dir = DigitalbitsCoreBackup::Utils.create_working_dir(@config.get('working_dir'))
23
+ @cmd = DigitalbitsCoreBackup::Cmd.new(@working_dir)
24
+ @select = args[:select] if args.has_key?(:select)
25
+ @s3 = DigitalbitsCoreBackup::S3.new(@config)
26
+ @pushgateway_url = @config.get('pushgateway_url')
27
+
28
+ # Set per operation type run time parameters
29
+ if args.has_key?(:type) then
30
+ case args[:type]
31
+ when 'backup'
32
+ puts 'info: backing up digitalbits-core'
33
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_start_time')
34
+ @backup_dir = DigitalbitsCoreBackup::Utils.create_backup_dir(@config.get('backup_dir'))
35
+ @db = DigitalbitsCoreBackup::Database.new(@config)
36
+ @fs = DigitalbitsCoreBackup::Filesystem.new(@config)
37
+ when 'restore'
38
+ puts 'info: restoring digitalbits-core'
39
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_start_time')
40
+ @db_restore = DigitalbitsCoreBackup::Restore::Database.new(@config)
41
+ @fs_restore = DigitalbitsCoreBackup::Restore::Filesystem.new(@config)
42
+ @utils = DigitalbitsCoreBackup::Utils.new(@config)
43
+ when 'getkey'
44
+ puts 'info: confirming public gpg key with key server'
45
+ when 'list'
46
+ puts "info: listing last #{@listlen} digitalbits-core backups"
47
+ end
48
+ end
49
+ end
50
+
51
+ def run()
52
+ case @job_type
53
+ when 'getkey'
54
+ begin
55
+ getkey = @cmd.run_and_capture('gpg', ['--keyserver', 'hkp://ipv4.pool.sks-keyservers.net', '--recv-key', @gpg_key, '2>&1'])
56
+ puts 'info: public gpg key installed'
57
+ if ! getkey.success then
58
+ puts "error: failed to get gpg key"
59
+ # dump the gpg output here for user level trouble shooting
60
+ puts "#{getkey.out}"
61
+ raise StandardError
62
+ end
63
+ rescue => e
64
+ puts e
65
+ end
66
+ when 'list'
67
+ begin
68
+ list=@s3.latest(@listlen)
69
+ puts "info: only #{list.length} backup files in bucket" if list.length < @listlen
70
+ puts list
71
+ rescue => e
72
+ puts e
73
+ end
74
+ when 'backup'
75
+ begin
76
+ if !DigitalbitsCoreBackup::Utils.core_healthy?(@config) then
77
+ puts "error: Can't back up unhealthy digitalbits-core"
78
+ raise StandardError
79
+ end
80
+ puts 'info: stopping digitalbits-core'
81
+ # using sudo, if running as non root uid then you will need to configure sudoers
82
+ stop_core = @cmd.run_and_capture('sudo', ['/bin/systemctl', 'stop', 'digitalbits-core'])
83
+ # only proceed if core is stopped
84
+ if stop_core.success then
85
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_db_dump_start_time')
86
+ @db.backup
87
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_db_dump_finish_time')
88
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_fs_backup_start_time')
89
+ @fs.backup
90
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_fs_backup_finish_time')
91
+ if @verify then
92
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_verify_start_time')
93
+ create_hash_file = @cmd.run_and_capture('find', ['.', '-type', 'f', '!', '-name', 'SHA256SUMS', '|', 'xargs', 'sha256sum', '>', 'SHA256SUMS'])
94
+ if create_hash_file.success then
95
+ puts "info: sha sums file created"
96
+ else
97
+ puts 'error: error creating sha sums file'
98
+ raise StandardError
99
+ end
100
+ sign_hash_file = @cmd.run_and_capture('gpg', ['--local-user', @gpg_key, '--detach-sign', 'SHA256SUMS'])
101
+ if sign_hash_file.success then
102
+ puts "info: gpg signature created ok"
103
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_verify_finish_time')
104
+ else
105
+ puts 'error: error signing sha256sum file'
106
+ raise StandardError
107
+ end
108
+ end
109
+ # create tar archive with fs, db backup files and if requested the file of shas256sums and corresponding gpg signature.
110
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_tar_start_time')
111
+ @backup = DigitalbitsCoreBackup::Utils.create_backup_tar(@working_dir, @backup_dir)
112
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_tar_finish_time')
113
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_s3_push_start_time')
114
+ @s3.push(@backup)
115
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_s3_push_finish_time')
116
+ else
117
+ puts 'error: can not stop digitalbits-core'
118
+ raise StandardError
119
+ end
120
+
121
+ # restart digitalbits-core post backup
122
+ puts 'info: starting digitalbits-core'
123
+ # using sudo, if running as non root uid then you will need to configure sudoers
124
+ start_core = @cmd.run_and_capture('sudo', ['/bin/systemctl', 'start', 'digitalbits-core'])
125
+ if start_core.success then
126
+ puts "info: digitalbits-core started"
127
+ else
128
+ puts 'error: can not start digitalbits-core'
129
+ raise StandardError
130
+ end
131
+
132
+ # clean up working_dir
133
+ DigitalbitsCoreBackup::Utils.cleanup(@working_dir)
134
+ rescue => e
135
+ puts e
136
+ # clean up working_dir
137
+ DigitalbitsCoreBackup::Utils.cleanup(@working_dir)
138
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_fail_time')
139
+ else
140
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_backup_success_time')
141
+ end
142
+ when 'restore'
143
+ begin
144
+ # confirm the bucket directory is set to be cleaned or is empty
145
+ # the fs_restore.core_data_dir_empty? throws an exception if it's not empty
146
+ if ! @clean then
147
+ @fs_restore.core_data_dir_empty?()
148
+ end
149
+ # using sudo, if running as non root uid then you will need to configure sudoers
150
+ stop_core = @cmd.run_and_capture('sudo', ['/bin/systemctl', 'stop', 'digitalbits-core'])
151
+ # only proceed if core is stopped
152
+ if stop_core.success then
153
+ # if no manual selection has been made, use the latest as derived from the s3.latest method
154
+ # this method returns an array so set @select to the first and only element
155
+ @select=@s3.latest(1)[0] if ! @select
156
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_s3_get_start_time')
157
+ @backup_archive = @s3.get(@select)
158
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_s3_get_finish_time')
159
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_untar_start_time')
160
+ @utils.extract_backup(@backup_archive)
161
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_untar_finish_time')
162
+ if @verify then
163
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_verify_start_time')
164
+ verify_hash_file = @cmd.run_and_capture('gpg', ['--local-user', @gpg_key, '--verify', 'SHA256SUMS.sig', 'SHA256SUMS', '2>&1'])
165
+ if verify_hash_file.success then
166
+ puts "info: gpg signature processed ok"
167
+ else
168
+ puts 'error: error verifying gpg signature'
169
+ raise StandardError
170
+ end
171
+ verify_sha_file_content = @cmd.run_and_capture('sha256sum', ['--status', '--strict', '-c', 'SHA256SUMS'])
172
+ if verify_sha_file_content.success then
173
+ puts "info: sha file sums match"
174
+ else
175
+ puts 'error: error processing sha256sum file'
176
+ raise StandardError
177
+ end
178
+ if DigitalbitsCoreBackup::Utils.confirm_shasums_definitive(@working_dir, @backup_archive) then
179
+ puts 'info: SHA256SUMS file list matches delivered archive'
180
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_verify_finish_time')
181
+ else
182
+ puts 'error: unknown additional file(s) detected in archive'
183
+ raise StandardError
184
+ end
185
+ end
186
+ DigitalbitsCoreBackup::Utils.cleanbucket(@fs_restore.core_data_dir) if @clean
187
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_fs_restore_start_time')
188
+ @fs_restore.restore(@backup_archive)
189
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_fs_restore_finish_time')
190
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_db_restore_start_time')
191
+ @db_restore.restore()
192
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_db_restore_finish_time')
193
+
194
+ # restart digitalbits-core post restore
195
+ puts 'info: starting digitalbits-core'
196
+ # using sudo, if running as non root uid then you will need to configure sudoers
197
+ start_core = @cmd.run_and_capture('sudo', ['/bin/systemctl', 'start', 'digitalbits-core'])
198
+ if start_core.success then
199
+ puts "info: digitalbits-core started"
200
+ else
201
+ puts 'error: can not start digitalbits-core'
202
+ raise StandardError
203
+ end
204
+ else
205
+ puts 'error: can not stop digitalbits-core'
206
+ raise StandardError
207
+ end
208
+
209
+ # clean up working_dir
210
+ DigitalbitsCoreBackup::Utils.cleanup(@working_dir)
211
+ rescue => e
212
+ puts e
213
+ # clean up working_dir
214
+ DigitalbitsCoreBackup::Utils.cleanup(@working_dir)
215
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_fail_time')
216
+ else
217
+ DigitalbitsCoreBackup::Utils.push_metric(@pushgateway_url, 'digitalbits_core_restore_success_time')
218
+ end
219
+ end
220
+ end
221
+
222
+ end
223
+ end