backupii 0.1.0.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +37 -0
  4. data/bin/backupii +5 -0
  5. data/bin/docker_test +24 -0
  6. data/lib/backup/archive.rb +171 -0
  7. data/lib/backup/binder.rb +23 -0
  8. data/lib/backup/cleaner.rb +114 -0
  9. data/lib/backup/cli.rb +376 -0
  10. data/lib/backup/cloud_io/base.rb +40 -0
  11. data/lib/backup/cloud_io/cloud_files.rb +301 -0
  12. data/lib/backup/cloud_io/s3.rb +256 -0
  13. data/lib/backup/compressor/base.rb +34 -0
  14. data/lib/backup/compressor/bzip2.rb +37 -0
  15. data/lib/backup/compressor/custom.rb +51 -0
  16. data/lib/backup/compressor/gzip.rb +76 -0
  17. data/lib/backup/config/dsl.rb +103 -0
  18. data/lib/backup/config/helpers.rb +139 -0
  19. data/lib/backup/config.rb +122 -0
  20. data/lib/backup/database/base.rb +89 -0
  21. data/lib/backup/database/mongodb.rb +189 -0
  22. data/lib/backup/database/mysql.rb +194 -0
  23. data/lib/backup/database/openldap.rb +97 -0
  24. data/lib/backup/database/postgresql.rb +134 -0
  25. data/lib/backup/database/redis.rb +179 -0
  26. data/lib/backup/database/riak.rb +82 -0
  27. data/lib/backup/database/sqlite.rb +57 -0
  28. data/lib/backup/encryptor/base.rb +29 -0
  29. data/lib/backup/encryptor/gpg.rb +745 -0
  30. data/lib/backup/encryptor/open_ssl.rb +76 -0
  31. data/lib/backup/errors.rb +55 -0
  32. data/lib/backup/logger/console.rb +50 -0
  33. data/lib/backup/logger/fog_adapter.rb +27 -0
  34. data/lib/backup/logger/logfile.rb +134 -0
  35. data/lib/backup/logger/syslog.rb +116 -0
  36. data/lib/backup/logger.rb +199 -0
  37. data/lib/backup/model.rb +478 -0
  38. data/lib/backup/notifier/base.rb +128 -0
  39. data/lib/backup/notifier/campfire.rb +63 -0
  40. data/lib/backup/notifier/command.rb +101 -0
  41. data/lib/backup/notifier/datadog.rb +107 -0
  42. data/lib/backup/notifier/flowdock.rb +101 -0
  43. data/lib/backup/notifier/hipchat.rb +118 -0
  44. data/lib/backup/notifier/http_post.rb +116 -0
  45. data/lib/backup/notifier/mail.rb +235 -0
  46. data/lib/backup/notifier/nagios.rb +67 -0
  47. data/lib/backup/notifier/pagerduty.rb +82 -0
  48. data/lib/backup/notifier/prowl.rb +70 -0
  49. data/lib/backup/notifier/pushover.rb +73 -0
  50. data/lib/backup/notifier/ses.rb +126 -0
  51. data/lib/backup/notifier/slack.rb +149 -0
  52. data/lib/backup/notifier/twitter.rb +57 -0
  53. data/lib/backup/notifier/zabbix.rb +62 -0
  54. data/lib/backup/package.rb +53 -0
  55. data/lib/backup/packager.rb +108 -0
  56. data/lib/backup/pipeline.rb +122 -0
  57. data/lib/backup/splitter.rb +75 -0
  58. data/lib/backup/storage/base.rb +72 -0
  59. data/lib/backup/storage/cloud_files.rb +158 -0
  60. data/lib/backup/storage/cycler.rb +73 -0
  61. data/lib/backup/storage/dropbox.rb +208 -0
  62. data/lib/backup/storage/ftp.rb +118 -0
  63. data/lib/backup/storage/local.rb +63 -0
  64. data/lib/backup/storage/qiniu.rb +68 -0
  65. data/lib/backup/storage/rsync.rb +251 -0
  66. data/lib/backup/storage/s3.rb +157 -0
  67. data/lib/backup/storage/scp.rb +67 -0
  68. data/lib/backup/storage/sftp.rb +82 -0
  69. data/lib/backup/syncer/base.rb +70 -0
  70. data/lib/backup/syncer/cloud/base.rb +180 -0
  71. data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
  72. data/lib/backup/syncer/cloud/local_file.rb +99 -0
  73. data/lib/backup/syncer/cloud/s3.rb +118 -0
  74. data/lib/backup/syncer/rsync/base.rb +55 -0
  75. data/lib/backup/syncer/rsync/local.rb +29 -0
  76. data/lib/backup/syncer/rsync/pull.rb +49 -0
  77. data/lib/backup/syncer/rsync/push.rb +206 -0
  78. data/lib/backup/template.rb +45 -0
  79. data/lib/backup/utilities.rb +235 -0
  80. data/lib/backup/version.rb +5 -0
  81. data/lib/backup.rb +141 -0
  82. data/templates/cli/archive +28 -0
  83. data/templates/cli/compressor/bzip2 +4 -0
  84. data/templates/cli/compressor/custom +7 -0
  85. data/templates/cli/compressor/gzip +4 -0
  86. data/templates/cli/config +123 -0
  87. data/templates/cli/databases/mongodb +15 -0
  88. data/templates/cli/databases/mysql +18 -0
  89. data/templates/cli/databases/openldap +24 -0
  90. data/templates/cli/databases/postgresql +16 -0
  91. data/templates/cli/databases/redis +16 -0
  92. data/templates/cli/databases/riak +17 -0
  93. data/templates/cli/databases/sqlite +11 -0
  94. data/templates/cli/encryptor/gpg +27 -0
  95. data/templates/cli/encryptor/openssl +9 -0
  96. data/templates/cli/model +26 -0
  97. data/templates/cli/notifier/zabbix +15 -0
  98. data/templates/cli/notifiers/campfire +12 -0
  99. data/templates/cli/notifiers/command +32 -0
  100. data/templates/cli/notifiers/datadog +57 -0
  101. data/templates/cli/notifiers/flowdock +16 -0
  102. data/templates/cli/notifiers/hipchat +16 -0
  103. data/templates/cli/notifiers/http_post +32 -0
  104. data/templates/cli/notifiers/mail +24 -0
  105. data/templates/cli/notifiers/nagios +13 -0
  106. data/templates/cli/notifiers/pagerduty +12 -0
  107. data/templates/cli/notifiers/prowl +11 -0
  108. data/templates/cli/notifiers/pushover +11 -0
  109. data/templates/cli/notifiers/ses +15 -0
  110. data/templates/cli/notifiers/slack +22 -0
  111. data/templates/cli/notifiers/twitter +13 -0
  112. data/templates/cli/splitter +7 -0
  113. data/templates/cli/storages/cloud_files +11 -0
  114. data/templates/cli/storages/dropbox +20 -0
  115. data/templates/cli/storages/ftp +13 -0
  116. data/templates/cli/storages/local +8 -0
  117. data/templates/cli/storages/qiniu +12 -0
  118. data/templates/cli/storages/rsync +17 -0
  119. data/templates/cli/storages/s3 +16 -0
  120. data/templates/cli/storages/scp +15 -0
  121. data/templates/cli/storages/sftp +15 -0
  122. data/templates/cli/syncers/cloud_files +22 -0
  123. data/templates/cli/syncers/rsync_local +20 -0
  124. data/templates/cli/syncers/rsync_pull +28 -0
  125. data/templates/cli/syncers/rsync_push +28 -0
  126. data/templates/cli/syncers/s3 +27 -0
  127. data/templates/general/links +3 -0
  128. data/templates/general/version.erb +2 -0
  129. data/templates/notifier/mail/failure.erb +16 -0
  130. data/templates/notifier/mail/success.erb +16 -0
  131. data/templates/notifier/mail/warning.erb +16 -0
  132. data/templates/storage/dropbox/authorization_url.erb +6 -0
  133. data/templates/storage/dropbox/authorized.erb +4 -0
  134. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  135. metadata +507 -0
@@ -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