backupii 0.1.0.pre.alpha.1
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 +7 -0
- data/LICENSE +19 -0
- data/README.md +37 -0
- data/bin/backupii +5 -0
- data/bin/docker_test +24 -0
- data/lib/backup/archive.rb +171 -0
- data/lib/backup/binder.rb +23 -0
- data/lib/backup/cleaner.rb +114 -0
- data/lib/backup/cli.rb +376 -0
- data/lib/backup/cloud_io/base.rb +40 -0
- data/lib/backup/cloud_io/cloud_files.rb +301 -0
- data/lib/backup/cloud_io/s3.rb +256 -0
- data/lib/backup/compressor/base.rb +34 -0
- data/lib/backup/compressor/bzip2.rb +37 -0
- data/lib/backup/compressor/custom.rb +51 -0
- data/lib/backup/compressor/gzip.rb +76 -0
- data/lib/backup/config/dsl.rb +103 -0
- data/lib/backup/config/helpers.rb +139 -0
- data/lib/backup/config.rb +122 -0
- data/lib/backup/database/base.rb +89 -0
- data/lib/backup/database/mongodb.rb +189 -0
- data/lib/backup/database/mysql.rb +194 -0
- data/lib/backup/database/openldap.rb +97 -0
- data/lib/backup/database/postgresql.rb +134 -0
- data/lib/backup/database/redis.rb +179 -0
- data/lib/backup/database/riak.rb +82 -0
- data/lib/backup/database/sqlite.rb +57 -0
- data/lib/backup/encryptor/base.rb +29 -0
- data/lib/backup/encryptor/gpg.rb +745 -0
- data/lib/backup/encryptor/open_ssl.rb +76 -0
- data/lib/backup/errors.rb +55 -0
- data/lib/backup/logger/console.rb +50 -0
- data/lib/backup/logger/fog_adapter.rb +27 -0
- data/lib/backup/logger/logfile.rb +134 -0
- data/lib/backup/logger/syslog.rb +116 -0
- data/lib/backup/logger.rb +199 -0
- data/lib/backup/model.rb +478 -0
- data/lib/backup/notifier/base.rb +128 -0
- data/lib/backup/notifier/campfire.rb +63 -0
- data/lib/backup/notifier/command.rb +101 -0
- data/lib/backup/notifier/datadog.rb +107 -0
- data/lib/backup/notifier/flowdock.rb +101 -0
- data/lib/backup/notifier/hipchat.rb +118 -0
- data/lib/backup/notifier/http_post.rb +116 -0
- data/lib/backup/notifier/mail.rb +235 -0
- data/lib/backup/notifier/nagios.rb +67 -0
- data/lib/backup/notifier/pagerduty.rb +82 -0
- data/lib/backup/notifier/prowl.rb +70 -0
- data/lib/backup/notifier/pushover.rb +73 -0
- data/lib/backup/notifier/ses.rb +126 -0
- data/lib/backup/notifier/slack.rb +149 -0
- data/lib/backup/notifier/twitter.rb +57 -0
- data/lib/backup/notifier/zabbix.rb +62 -0
- data/lib/backup/package.rb +53 -0
- data/lib/backup/packager.rb +108 -0
- data/lib/backup/pipeline.rb +122 -0
- data/lib/backup/splitter.rb +75 -0
- data/lib/backup/storage/base.rb +72 -0
- data/lib/backup/storage/cloud_files.rb +158 -0
- data/lib/backup/storage/cycler.rb +73 -0
- data/lib/backup/storage/dropbox.rb +208 -0
- data/lib/backup/storage/ftp.rb +118 -0
- data/lib/backup/storage/local.rb +63 -0
- data/lib/backup/storage/qiniu.rb +68 -0
- data/lib/backup/storage/rsync.rb +251 -0
- data/lib/backup/storage/s3.rb +157 -0
- data/lib/backup/storage/scp.rb +67 -0
- data/lib/backup/storage/sftp.rb +82 -0
- data/lib/backup/syncer/base.rb +70 -0
- data/lib/backup/syncer/cloud/base.rb +180 -0
- data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
- data/lib/backup/syncer/cloud/local_file.rb +99 -0
- data/lib/backup/syncer/cloud/s3.rb +118 -0
- data/lib/backup/syncer/rsync/base.rb +55 -0
- data/lib/backup/syncer/rsync/local.rb +29 -0
- data/lib/backup/syncer/rsync/pull.rb +49 -0
- data/lib/backup/syncer/rsync/push.rb +206 -0
- data/lib/backup/template.rb +45 -0
- data/lib/backup/utilities.rb +235 -0
- data/lib/backup/version.rb +5 -0
- data/lib/backup.rb +141 -0
- data/templates/cli/archive +28 -0
- data/templates/cli/compressor/bzip2 +4 -0
- data/templates/cli/compressor/custom +7 -0
- data/templates/cli/compressor/gzip +4 -0
- data/templates/cli/config +123 -0
- data/templates/cli/databases/mongodb +15 -0
- data/templates/cli/databases/mysql +18 -0
- data/templates/cli/databases/openldap +24 -0
- data/templates/cli/databases/postgresql +16 -0
- data/templates/cli/databases/redis +16 -0
- data/templates/cli/databases/riak +17 -0
- data/templates/cli/databases/sqlite +11 -0
- data/templates/cli/encryptor/gpg +27 -0
- data/templates/cli/encryptor/openssl +9 -0
- data/templates/cli/model +26 -0
- data/templates/cli/notifier/zabbix +15 -0
- data/templates/cli/notifiers/campfire +12 -0
- data/templates/cli/notifiers/command +32 -0
- data/templates/cli/notifiers/datadog +57 -0
- data/templates/cli/notifiers/flowdock +16 -0
- data/templates/cli/notifiers/hipchat +16 -0
- data/templates/cli/notifiers/http_post +32 -0
- data/templates/cli/notifiers/mail +24 -0
- data/templates/cli/notifiers/nagios +13 -0
- data/templates/cli/notifiers/pagerduty +12 -0
- data/templates/cli/notifiers/prowl +11 -0
- data/templates/cli/notifiers/pushover +11 -0
- data/templates/cli/notifiers/ses +15 -0
- data/templates/cli/notifiers/slack +22 -0
- data/templates/cli/notifiers/twitter +13 -0
- data/templates/cli/splitter +7 -0
- data/templates/cli/storages/cloud_files +11 -0
- data/templates/cli/storages/dropbox +20 -0
- data/templates/cli/storages/ftp +13 -0
- data/templates/cli/storages/local +8 -0
- data/templates/cli/storages/qiniu +12 -0
- data/templates/cli/storages/rsync +17 -0
- data/templates/cli/storages/s3 +16 -0
- data/templates/cli/storages/scp +15 -0
- data/templates/cli/storages/sftp +15 -0
- data/templates/cli/syncers/cloud_files +22 -0
- data/templates/cli/syncers/rsync_local +20 -0
- data/templates/cli/syncers/rsync_pull +28 -0
- data/templates/cli/syncers/rsync_push +28 -0
- data/templates/cli/syncers/s3 +27 -0
- data/templates/general/links +3 -0
- data/templates/general/version.erb +2 -0
- data/templates/notifier/mail/failure.erb +16 -0
- data/templates/notifier/mail/success.erb +16 -0
- data/templates/notifier/mail/warning.erb +16 -0
- data/templates/storage/dropbox/authorization_url.erb +6 -0
- data/templates/storage/dropbox/authorized.erb +4 -0
- data/templates/storage/dropbox/cache_file_written.erb +10 -0
- metadata +507 -0
data/lib/backup/model.rb
ADDED
@@ -0,0 +1,478 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
class Model
|
5
|
+
class Error < Backup::Error; end
|
6
|
+
class FatalError < Backup::FatalError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
##
|
10
|
+
# The Backup::Model.all class method keeps track of all the models
|
11
|
+
# that have been instantiated. It returns the @all class variable,
|
12
|
+
# which contains an array of all the models
|
13
|
+
def all
|
14
|
+
@all ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Return an Array of Models matching the given +trigger+.
|
19
|
+
def find_by_trigger(trigger)
|
20
|
+
trigger = trigger.to_s
|
21
|
+
if trigger.include?("*")
|
22
|
+
regex = %r{^#{trigger.gsub('*', '(.*)')}$}
|
23
|
+
all.select { |model| regex =~ model.trigger }
|
24
|
+
else
|
25
|
+
all.select { |model| trigger == model.trigger }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Allows users to create preconfigured models.
|
30
|
+
def preconfigure(&block)
|
31
|
+
@preconfigure ||= block
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# used for testing
|
37
|
+
def reset!
|
38
|
+
@all = @preconfigure = nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# The trigger (stored as a String) is used as an identifier
|
44
|
+
# for initializing the backup process
|
45
|
+
attr_reader :trigger
|
46
|
+
|
47
|
+
##
|
48
|
+
# The label (stored as a String) is used for a more friendly user output
|
49
|
+
attr_reader :label
|
50
|
+
|
51
|
+
##
|
52
|
+
# Array of configured Database objects.
|
53
|
+
attr_reader :databases
|
54
|
+
|
55
|
+
##
|
56
|
+
# Array of configured Archive objects.
|
57
|
+
attr_reader :archives
|
58
|
+
|
59
|
+
##
|
60
|
+
# Array of configured Notifier objects.
|
61
|
+
attr_reader :notifiers
|
62
|
+
|
63
|
+
##
|
64
|
+
# Array of configured Storage objects.
|
65
|
+
attr_reader :storages
|
66
|
+
|
67
|
+
##
|
68
|
+
# Array of configured Syncer objects.
|
69
|
+
attr_reader :syncers
|
70
|
+
|
71
|
+
##
|
72
|
+
# The configured Compressor, if any.
|
73
|
+
attr_reader :compressor
|
74
|
+
|
75
|
+
##
|
76
|
+
# The configured Encryptor, if any.
|
77
|
+
attr_reader :encryptor
|
78
|
+
|
79
|
+
##
|
80
|
+
# The configured Splitter, if any.
|
81
|
+
attr_reader :splitter
|
82
|
+
|
83
|
+
##
|
84
|
+
# The final backup Package this model will create.
|
85
|
+
attr_reader :package
|
86
|
+
|
87
|
+
##
|
88
|
+
# The time when the backup initiated (in format: 2011.02.20.03.29.59)
|
89
|
+
attr_reader :time
|
90
|
+
|
91
|
+
##
|
92
|
+
# The time when the backup initiated (as a Time object)
|
93
|
+
attr_reader :started_at
|
94
|
+
|
95
|
+
##
|
96
|
+
# The time when the backup finished (as a Time object)
|
97
|
+
attr_reader :finished_at
|
98
|
+
|
99
|
+
##
|
100
|
+
# Result of this model's backup process.
|
101
|
+
#
|
102
|
+
# 0 = Job was successful
|
103
|
+
# 1 = Job was successful, but issued warnings
|
104
|
+
# 2 = Job failed, additional triggers may be performed
|
105
|
+
# 3 = Job failed, additional triggers will not be performed
|
106
|
+
attr_reader :exit_status
|
107
|
+
|
108
|
+
##
|
109
|
+
# Exception raised by either a +before+ hook or one of the model's
|
110
|
+
# procedures that caused the model to fail. An exception raised by an
|
111
|
+
# +after+ hook would not be stored here. Therefore, it is possible for
|
112
|
+
# this to be +nil+ even if #exit_status is 2 or 3.
|
113
|
+
attr_reader :exception
|
114
|
+
|
115
|
+
def initialize(trigger, label, &block)
|
116
|
+
@trigger = trigger.to_s
|
117
|
+
@label = label.to_s
|
118
|
+
@package = Package.new(self)
|
119
|
+
|
120
|
+
@databases = []
|
121
|
+
@archives = []
|
122
|
+
@storages = []
|
123
|
+
@notifiers = []
|
124
|
+
@syncers = []
|
125
|
+
|
126
|
+
instance_eval(&self.class.preconfigure) if self.class.preconfigure
|
127
|
+
instance_eval(&block) if block_given?
|
128
|
+
|
129
|
+
# trigger all defined databases to generate their #dump_filename
|
130
|
+
# so warnings may be logged if `backup perform --check` is used
|
131
|
+
databases.each { |db| db.send(:dump_filename) }
|
132
|
+
|
133
|
+
Model.all << self
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Adds an Archive. Multiple Archives may be added to the model.
|
138
|
+
def archive(name, &block)
|
139
|
+
@archives << Archive.new(self, name, &block)
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Adds an Database. Multiple Databases may be added to the model.
|
144
|
+
def database(name, database_id = nil, &block)
|
145
|
+
@databases << get_class_from_scope(Database, name)
|
146
|
+
.new(self, database_id, &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Adds an Storage. Multiple Storages may be added to the model.
|
151
|
+
def store_with(name, storage_id = nil, &block)
|
152
|
+
@storages << get_class_from_scope(Storage, name)
|
153
|
+
.new(self, storage_id, &block)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Adds an Syncer. Multiple Syncers may be added to the model.
|
158
|
+
def sync_with(name, syncer_id = nil, &block)
|
159
|
+
@syncers << get_class_from_scope(Syncer, name).new(syncer_id, &block)
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Adds an Notifier. Multiple Notifiers may be added to the model.
|
164
|
+
def notify_by(name, &block)
|
165
|
+
@notifiers << get_class_from_scope(Notifier, name).new(self, &block)
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# Adds an Encryptor. Only one Encryptor may be added to the model.
|
170
|
+
# This will be used to encrypt the final backup package.
|
171
|
+
def encrypt_with(name, &block)
|
172
|
+
@encryptor = get_class_from_scope(Encryptor, name).new(&block)
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# Adds an Compressor. Only one Compressor may be added to the model.
|
177
|
+
# This will be used to compress each individual Archive and Database
|
178
|
+
# stored within the final backup package.
|
179
|
+
def compress_with(name, &block)
|
180
|
+
@compressor = get_class_from_scope(Compressor, name).new(&block)
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Adds a Splitter to split the final backup package into multiple files.
|
185
|
+
#
|
186
|
+
# +chunk_size+ is specified in MiB and must be given as an Integer.
|
187
|
+
# +suffix_length+ controls the number of characters used in the suffix
|
188
|
+
# (and the maximum number of chunks possible).
|
189
|
+
# ie. 1 (-a, -b), 2 (-aa, -ab), 3 (-aaa, -aab)
|
190
|
+
def split_into_chunks_of(chunk_size, suffix_length = 3)
|
191
|
+
if chunk_size.is_a?(Integer) && suffix_length.is_a?(Integer)
|
192
|
+
@splitter = Splitter.new(self, chunk_size, suffix_length)
|
193
|
+
else
|
194
|
+
raise Error, <<-EOS
|
195
|
+
Invalid arguments for #split_into_chunks_of()
|
196
|
+
+chunk_size+ (and optional +suffix_length+) must be Integers.
|
197
|
+
EOS
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
##
|
202
|
+
# Defines a block of code to run before the model's procedures.
|
203
|
+
#
|
204
|
+
# Warnings logged within the before hook will elevate the model's
|
205
|
+
# exit_status to 1 and cause warning notifications to be sent.
|
206
|
+
#
|
207
|
+
# Raising an exception will abort the model and cause failure notifications
|
208
|
+
# to be sent. If the exception is a StandardError, exit_status will be 2.
|
209
|
+
# If the exception is not a StandardError, exit_status will be 3.
|
210
|
+
#
|
211
|
+
# If any exception is raised, any defined +after+ hook will be skipped.
|
212
|
+
def before(&block)
|
213
|
+
@before = block if block
|
214
|
+
@before
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Defines a block of code to run after the model's procedures.
|
219
|
+
#
|
220
|
+
# This code is ensured to run, even if the model failed, **unless** a
|
221
|
+
# +before+ hook raised an exception and aborted the model.
|
222
|
+
#
|
223
|
+
# The code block will be passed the model's current exit_status:
|
224
|
+
#
|
225
|
+
# `0`: Success, no warnings.
|
226
|
+
# `1`: Success, but warnings were logged.
|
227
|
+
# `2`: Failure, but additional models/triggers will still be processed.
|
228
|
+
# `3`: Failure, no additional models/triggers will be processed.
|
229
|
+
#
|
230
|
+
# The model's exit_status may be elevated based on the after hook's
|
231
|
+
# actions, but will never be decreased.
|
232
|
+
#
|
233
|
+
# Warnings logged within the after hook may elevate the model's
|
234
|
+
# exit_status to 1 and cause warning notifications to be sent.
|
235
|
+
#
|
236
|
+
# Raising an exception may elevate the model's exit_status and cause
|
237
|
+
# failure notifications to be sent. If the exception is a StandardError,
|
238
|
+
# the exit_status will be elevated to 2. If the exception is not a
|
239
|
+
# StandardError, the exit_status will be elevated to 3.
|
240
|
+
def after(&block)
|
241
|
+
@after = block if block
|
242
|
+
@after
|
243
|
+
end
|
244
|
+
|
245
|
+
##
|
246
|
+
# Performs the backup process
|
247
|
+
#
|
248
|
+
# Once complete, #exit_status will indicate the result of this process.
|
249
|
+
#
|
250
|
+
# If any errors occur during the backup process, all temporary files will
|
251
|
+
# be left in place. If the error occurs before Packaging, then the
|
252
|
+
# temporary folder (tmp_path/trigger) will remain and may contain all or
|
253
|
+
# some of the configured Archives and/or Database dumps. If the error
|
254
|
+
# occurs after Packaging, but before the Storages complete, then the final
|
255
|
+
# packaged files (located in the root of tmp_path) will remain.
|
256
|
+
#
|
257
|
+
# *** Important ***
|
258
|
+
# If an error occurs and any of the above mentioned temporary files remain,
|
259
|
+
# those files *** will be removed *** before the next scheduled backup for
|
260
|
+
# the same trigger.
|
261
|
+
def perform!
|
262
|
+
@started_at = Time.now.utc
|
263
|
+
@time = package.time = started_at.strftime("%Y.%m.%d.%H.%M.%S")
|
264
|
+
|
265
|
+
log!(:started)
|
266
|
+
before_hook
|
267
|
+
|
268
|
+
procedures.each do |procedure|
|
269
|
+
procedure.is_a?(Proc) ? procedure.call : procedure.each(&:perform!)
|
270
|
+
end
|
271
|
+
|
272
|
+
syncers.each(&:perform!)
|
273
|
+
rescue Interrupt
|
274
|
+
@interrupted = true
|
275
|
+
raise
|
276
|
+
rescue Exception => err
|
277
|
+
@exception = err
|
278
|
+
ensure
|
279
|
+
unless @interrupted
|
280
|
+
set_exit_status
|
281
|
+
@finished_at = Time.now.utc
|
282
|
+
log!(:finished)
|
283
|
+
after_hook
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
##
|
288
|
+
# The duration of the backup process (in format: HH:MM:SS)
|
289
|
+
def duration
|
290
|
+
return unless finished_at
|
291
|
+
|
292
|
+
elapsed_time(started_at, finished_at)
|
293
|
+
end
|
294
|
+
|
295
|
+
private
|
296
|
+
|
297
|
+
##
|
298
|
+
# Returns an array of procedures that will be performed if any
|
299
|
+
# Archives or Databases are configured for the model.
|
300
|
+
def procedures
|
301
|
+
return [] unless databases.any? || archives.any?
|
302
|
+
|
303
|
+
[-> { prepare! }, databases, archives,
|
304
|
+
-> { package! }, -> { store! }, -> { clean! }]
|
305
|
+
end
|
306
|
+
|
307
|
+
##
|
308
|
+
# Clean any temporary files and/or package files left over
|
309
|
+
# from the last time this model/trigger was performed.
|
310
|
+
# Logs warnings if files exist and are cleaned.
|
311
|
+
def prepare!
|
312
|
+
Cleaner.prepare(self)
|
313
|
+
end
|
314
|
+
|
315
|
+
##
|
316
|
+
# After all the databases and archives have been dumped and stored,
|
317
|
+
# these files will be bundled in to a .tar archive (uncompressed),
|
318
|
+
# which may be optionally Encrypted and/or Split into multiple "chunks".
|
319
|
+
# All information about this final archive is stored in the @package.
|
320
|
+
# Once complete, the temporary folder used during packaging is removed.
|
321
|
+
def package!
|
322
|
+
Packager.package!(self)
|
323
|
+
Cleaner.remove_packaging(self)
|
324
|
+
end
|
325
|
+
|
326
|
+
##
|
327
|
+
# Attempts to use all configured Storages, even if some of them result in
|
328
|
+
# exceptions. Returns true or raises first encountered exception.
|
329
|
+
def store!
|
330
|
+
storage_results = storages.map do |storage|
|
331
|
+
begin
|
332
|
+
storage.perform!
|
333
|
+
rescue => err
|
334
|
+
err
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
first_exception, *other_exceptions = storage_results.select do |result|
|
339
|
+
result.is_a? Exception
|
340
|
+
end
|
341
|
+
|
342
|
+
if first_exception
|
343
|
+
other_exceptions.each do |exception|
|
344
|
+
Logger.error exception.to_s
|
345
|
+
Logger.error exception.backtrace.join('\n')
|
346
|
+
end
|
347
|
+
raise first_exception
|
348
|
+
else
|
349
|
+
true
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
##
|
354
|
+
# Removes the final package file(s) once all configured Storages have run.
|
355
|
+
def clean!
|
356
|
+
Cleaner.remove_package(package)
|
357
|
+
end
|
358
|
+
|
359
|
+
##
|
360
|
+
# Returns the class/model specified by +name+ inside of +scope+.
|
361
|
+
# +scope+ should be a Class/Module.
|
362
|
+
# +name+ may be Class/Module or String representation
|
363
|
+
# of any namespace which exists under +scope+.
|
364
|
+
#
|
365
|
+
# The 'Backup::Config::DSL' namespace is stripped from +name+,
|
366
|
+
# since this is the namespace where we define module namespaces
|
367
|
+
# for use with Model's DSL methods.
|
368
|
+
#
|
369
|
+
# Examples:
|
370
|
+
# get_class_from_scope(Backup::Database, 'MySQL')
|
371
|
+
# returns the class Backup::Database::MySQL
|
372
|
+
#
|
373
|
+
# get_class_from_scope(Backup::Syncer, Backup::Config::RSync::Local)
|
374
|
+
# returns the class Backup::Syncer::RSync::Local
|
375
|
+
#
|
376
|
+
def get_class_from_scope(scope, name)
|
377
|
+
klass = scope
|
378
|
+
name = name.to_s.sub(%r{^Backup::Config::DSL::}, "")
|
379
|
+
name.split("::").each do |chunk|
|
380
|
+
klass = klass.const_get(chunk)
|
381
|
+
end
|
382
|
+
klass
|
383
|
+
end
|
384
|
+
|
385
|
+
##
|
386
|
+
# Sets or updates the model's #exit_status.
|
387
|
+
def set_exit_status
|
388
|
+
@exit_status =
|
389
|
+
if exception
|
390
|
+
exception.is_a?(StandardError) ? 2 : 3
|
391
|
+
else
|
392
|
+
Logger.has_warnings? ? 1 : 0
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
# Runs the +before+ hook.
|
398
|
+
# Any exception raised will be wrapped and re-raised, where it will be
|
399
|
+
# handled by #perform the same as an exception raised while performing
|
400
|
+
# the model's #procedures. Only difference is that an exception raised
|
401
|
+
# here will prevent any +after+ hook from being run.
|
402
|
+
def before_hook
|
403
|
+
return unless before
|
404
|
+
|
405
|
+
Logger.info "Before Hook Starting..."
|
406
|
+
before.call
|
407
|
+
Logger.info "Before Hook Finished."
|
408
|
+
rescue Exception => err
|
409
|
+
@before_hook_failed = true
|
410
|
+
ex = err.is_a?(StandardError) ? Error : FatalError
|
411
|
+
raise ex.wrap(err, "Before Hook Failed!")
|
412
|
+
end
|
413
|
+
|
414
|
+
##
|
415
|
+
# Runs the +after+ hook.
|
416
|
+
# Any exception raised here will be logged only and the model's
|
417
|
+
# #exit_status will be elevated if neccessary.
|
418
|
+
def after_hook
|
419
|
+
return unless after && !@before_hook_failed
|
420
|
+
|
421
|
+
Logger.info "After Hook Starting..."
|
422
|
+
after.call(exit_status)
|
423
|
+
Logger.info "After Hook Finished."
|
424
|
+
|
425
|
+
set_exit_status # in case hook logged warnings
|
426
|
+
rescue Exception => err
|
427
|
+
fatal = !err.is_a?(StandardError)
|
428
|
+
ex = fatal ? FatalError : Error
|
429
|
+
Logger.error ex.wrap(err, "After Hook Failed!")
|
430
|
+
# upgrade exit_status if needed
|
431
|
+
(@exit_status = fatal ? 3 : 2) unless exit_status == 3
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# Logs messages when the model starts and finishes.
|
436
|
+
#
|
437
|
+
# #exception will be set here if #exit_status is > 1,
|
438
|
+
# since log(:finished) is called before the +after+ hook.
|
439
|
+
def log!(action)
|
440
|
+
case action
|
441
|
+
when :started
|
442
|
+
Logger.info "Performing Backup for '#{label} (#{trigger})'!\n" \
|
443
|
+
"[ backup #{VERSION} : #{RUBY_DESCRIPTION} ]"
|
444
|
+
|
445
|
+
when :finished
|
446
|
+
if exit_status > 1
|
447
|
+
ex = exit_status == 2 ? Error : FatalError
|
448
|
+
err = ex.wrap(exception, "Backup for #{label} (#{trigger}) Failed!")
|
449
|
+
Logger.error err
|
450
|
+
Logger.error "\nBacktrace:\n\s\s" +
|
451
|
+
err.backtrace.join("\n\s\s") + "\n\n"
|
452
|
+
|
453
|
+
Cleaner.warnings(self)
|
454
|
+
else
|
455
|
+
msg = "Backup for '#{label} (#{trigger})' ".dup
|
456
|
+
if exit_status == 1
|
457
|
+
msg << "Completed Successfully (with Warnings) in #{duration}"
|
458
|
+
Logger.warn msg
|
459
|
+
else
|
460
|
+
msg << "Completed Successfully in #{duration}"
|
461
|
+
Logger.info msg
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
##
|
468
|
+
# Returns a string representing the elapsed time in HH:MM:SS.
|
469
|
+
def elapsed_time(start_time, finish_time)
|
470
|
+
duration = finish_time.to_i - start_time.to_i
|
471
|
+
hours = duration / 3600
|
472
|
+
remainder = duration - (hours * 3600)
|
473
|
+
minutes = remainder / 60
|
474
|
+
seconds = remainder - (minutes * 60)
|
475
|
+
sprintf "%02d:%02d:%02d", hours, minutes, seconds
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module Notifier
|
5
|
+
class Error < Backup::Error; end
|
6
|
+
|
7
|
+
class Base
|
8
|
+
include Utilities::Helpers
|
9
|
+
include Config::Helpers
|
10
|
+
|
11
|
+
##
|
12
|
+
# When set to true, the user will be notified by email
|
13
|
+
# when a backup process ends without raising any exceptions
|
14
|
+
attr_accessor :on_success
|
15
|
+
alias notify_on_success? on_success
|
16
|
+
|
17
|
+
##
|
18
|
+
# When set to true, the user will be notified by email
|
19
|
+
# when a backup process is successful, but has warnings
|
20
|
+
attr_accessor :on_warning
|
21
|
+
alias notify_on_warning? on_warning
|
22
|
+
|
23
|
+
##
|
24
|
+
# When set to true, the user will be notified by email
|
25
|
+
# when a backup process raises an exception before finishing
|
26
|
+
attr_accessor :on_failure
|
27
|
+
alias notify_on_failure? on_failure
|
28
|
+
|
29
|
+
##
|
30
|
+
# Number of times to retry failed attempts to send notification.
|
31
|
+
# Default: 10
|
32
|
+
attr_accessor :max_retries
|
33
|
+
|
34
|
+
##
|
35
|
+
# Time in seconds to pause before each retry.
|
36
|
+
# Default: 30
|
37
|
+
attr_accessor :retry_waitsec
|
38
|
+
|
39
|
+
##
|
40
|
+
# Message to send. Depends on notifier implementation if this is used.
|
41
|
+
# Default: lambda returning:
|
42
|
+
# "#{ message } #{ model.label } (#{ model.trigger })"
|
43
|
+
#
|
44
|
+
# @yieldparam [model] Backup::Model
|
45
|
+
# @yieldparam [data] Hash containing `message` and `key` values.
|
46
|
+
attr_accessor :message
|
47
|
+
|
48
|
+
attr_reader :model
|
49
|
+
|
50
|
+
def initialize(model)
|
51
|
+
@model = model
|
52
|
+
load_defaults!
|
53
|
+
|
54
|
+
@on_success = true if on_success.nil?
|
55
|
+
@on_warning = true if on_warning.nil?
|
56
|
+
@on_failure = true if on_failure.nil?
|
57
|
+
@max_retries ||= 10
|
58
|
+
@retry_waitsec ||= 30
|
59
|
+
@message ||= lambda do |m, data|
|
60
|
+
"[#{data[:status][:message]}] #{m.label} (#{m.trigger})"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# This method is called from an ensure block in Model#perform! and must
|
65
|
+
# not raise any exceptions. However, each Notifier's #notify! method
|
66
|
+
# should raise an exception if the request fails so it may be retried.
|
67
|
+
def perform!
|
68
|
+
status =
|
69
|
+
case model.exit_status
|
70
|
+
when 0
|
71
|
+
:success if notify_on_success?
|
72
|
+
when 1
|
73
|
+
:warning if notify_on_success? || notify_on_warning?
|
74
|
+
else
|
75
|
+
:failure if notify_on_failure?
|
76
|
+
end
|
77
|
+
|
78
|
+
if status
|
79
|
+
Logger.info "Sending notification using #{notifier_name}..."
|
80
|
+
with_retries { notify!(status) }
|
81
|
+
end
|
82
|
+
rescue Exception => err
|
83
|
+
Logger.error Error.wrap(err, "#{notifier_name} Failed!")
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def with_retries
|
89
|
+
retries = 0
|
90
|
+
begin
|
91
|
+
yield
|
92
|
+
rescue StandardError, Timeout::Error => err
|
93
|
+
retries += 1
|
94
|
+
raise if retries > max_retries
|
95
|
+
|
96
|
+
Logger.info Error.wrap(err, "Retry ##{retries} of #{max_retries}.")
|
97
|
+
sleep(retry_waitsec)
|
98
|
+
retry
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Return the notifier name, with Backup namespace removed
|
104
|
+
def notifier_name
|
105
|
+
self.class.to_s.sub("Backup::", "")
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Return status data for message creation
|
110
|
+
def status_data_for(status)
|
111
|
+
{
|
112
|
+
success: {
|
113
|
+
message: "Backup::Success",
|
114
|
+
key: :success
|
115
|
+
},
|
116
|
+
warning: {
|
117
|
+
message: "Backup::Warning",
|
118
|
+
key: :warning
|
119
|
+
},
|
120
|
+
failure: {
|
121
|
+
message: "Backup::Failure",
|
122
|
+
key: :failure
|
123
|
+
}
|
124
|
+
}[status]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Backup
|
6
|
+
module Notifier
|
7
|
+
class Campfire < Base
|
8
|
+
##
|
9
|
+
# Campfire api authentication token
|
10
|
+
attr_accessor :api_token
|
11
|
+
|
12
|
+
##
|
13
|
+
# Campfire account's subdomain
|
14
|
+
attr_accessor :subdomain
|
15
|
+
|
16
|
+
##
|
17
|
+
# Campfire account's room id
|
18
|
+
attr_accessor :room_id
|
19
|
+
|
20
|
+
def initialize(model, &block)
|
21
|
+
super
|
22
|
+
instance_eval(&block) if block_given?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
##
|
28
|
+
# Notify the user of the backup operation results.
|
29
|
+
#
|
30
|
+
# `status` indicates one of the following:
|
31
|
+
#
|
32
|
+
# `:success`
|
33
|
+
# : The backup completed successfully.
|
34
|
+
# : Notification will be sent if `on_success` is `true`.
|
35
|
+
#
|
36
|
+
# `:warning`
|
37
|
+
# : The backup completed successfully, but warnings were logged.
|
38
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
39
|
+
#
|
40
|
+
# `:failure`
|
41
|
+
# : The backup operation failed.
|
42
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
43
|
+
#
|
44
|
+
def notify!(status)
|
45
|
+
send_message(message.call(model, status: status_data_for(status)))
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_message(message)
|
49
|
+
uri = "https://#{subdomain}.campfirenow.com/room/#{room_id}/speak.json"
|
50
|
+
options = {
|
51
|
+
headers: { "Content-Type" => "application/json" },
|
52
|
+
body: JSON.dump(
|
53
|
+
message: { body: message, type: "Textmessage" }
|
54
|
+
)
|
55
|
+
}
|
56
|
+
options[:user] = api_token
|
57
|
+
options[:password] = "x" # Basic Auth
|
58
|
+
options[:expects] = 201 # raise error if unsuccessful
|
59
|
+
Excon.post(uri, options)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|