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