digitalbits-core-backup 0.0.7

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.
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