cm-backup 1.0.0

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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +20 -0
  3. data/bin/backup +5 -0
  4. data/lib/backup.rb +144 -0
  5. data/lib/backup/archive.rb +170 -0
  6. data/lib/backup/binder.rb +22 -0
  7. data/lib/backup/cleaner.rb +116 -0
  8. data/lib/backup/cli.rb +374 -0
  9. data/lib/backup/cloud_io/base.rb +41 -0
  10. data/lib/backup/cloud_io/cloud_files.rb +298 -0
  11. data/lib/backup/cloud_io/s3.rb +260 -0
  12. data/lib/backup/compressor/base.rb +35 -0
  13. data/lib/backup/compressor/bzip2.rb +39 -0
  14. data/lib/backup/compressor/custom.rb +53 -0
  15. data/lib/backup/compressor/gzip.rb +74 -0
  16. data/lib/backup/config.rb +119 -0
  17. data/lib/backup/config/dsl.rb +103 -0
  18. data/lib/backup/config/helpers.rb +143 -0
  19. data/lib/backup/database/base.rb +85 -0
  20. data/lib/backup/database/mongodb.rb +187 -0
  21. data/lib/backup/database/mysql.rb +192 -0
  22. data/lib/backup/database/openldap.rb +95 -0
  23. data/lib/backup/database/postgresql.rb +133 -0
  24. data/lib/backup/database/redis.rb +179 -0
  25. data/lib/backup/database/riak.rb +82 -0
  26. data/lib/backup/database/sqlite.rb +57 -0
  27. data/lib/backup/encryptor/base.rb +29 -0
  28. data/lib/backup/encryptor/gpg.rb +747 -0
  29. data/lib/backup/encryptor/open_ssl.rb +77 -0
  30. data/lib/backup/errors.rb +58 -0
  31. data/lib/backup/logger.rb +199 -0
  32. data/lib/backup/logger/console.rb +51 -0
  33. data/lib/backup/logger/fog_adapter.rb +29 -0
  34. data/lib/backup/logger/logfile.rb +133 -0
  35. data/lib/backup/logger/syslog.rb +116 -0
  36. data/lib/backup/model.rb +479 -0
  37. data/lib/backup/notifier/base.rb +128 -0
  38. data/lib/backup/notifier/campfire.rb +63 -0
  39. data/lib/backup/notifier/command.rb +102 -0
  40. data/lib/backup/notifier/datadog.rb +107 -0
  41. data/lib/backup/notifier/flowdock.rb +103 -0
  42. data/lib/backup/notifier/hipchat.rb +118 -0
  43. data/lib/backup/notifier/http_post.rb +117 -0
  44. data/lib/backup/notifier/mail.rb +249 -0
  45. data/lib/backup/notifier/nagios.rb +69 -0
  46. data/lib/backup/notifier/pagerduty.rb +81 -0
  47. data/lib/backup/notifier/prowl.rb +68 -0
  48. data/lib/backup/notifier/pushover.rb +74 -0
  49. data/lib/backup/notifier/ses.rb +105 -0
  50. data/lib/backup/notifier/slack.rb +148 -0
  51. data/lib/backup/notifier/twitter.rb +58 -0
  52. data/lib/backup/notifier/zabbix.rb +63 -0
  53. data/lib/backup/package.rb +55 -0
  54. data/lib/backup/packager.rb +107 -0
  55. data/lib/backup/pipeline.rb +124 -0
  56. data/lib/backup/splitter.rb +76 -0
  57. data/lib/backup/storage/base.rb +69 -0
  58. data/lib/backup/storage/cloud_files.rb +158 -0
  59. data/lib/backup/storage/cycler.rb +75 -0
  60. data/lib/backup/storage/dropbox.rb +212 -0
  61. data/lib/backup/storage/ftp.rb +112 -0
  62. data/lib/backup/storage/local.rb +64 -0
  63. data/lib/backup/storage/qiniu.rb +65 -0
  64. data/lib/backup/storage/rsync.rb +248 -0
  65. data/lib/backup/storage/s3.rb +156 -0
  66. data/lib/backup/storage/scp.rb +67 -0
  67. data/lib/backup/storage/sftp.rb +82 -0
  68. data/lib/backup/syncer/base.rb +70 -0
  69. data/lib/backup/syncer/cloud/base.rb +179 -0
  70. data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
  71. data/lib/backup/syncer/cloud/local_file.rb +100 -0
  72. data/lib/backup/syncer/cloud/s3.rb +110 -0
  73. data/lib/backup/syncer/rsync/base.rb +54 -0
  74. data/lib/backup/syncer/rsync/local.rb +31 -0
  75. data/lib/backup/syncer/rsync/pull.rb +51 -0
  76. data/lib/backup/syncer/rsync/push.rb +205 -0
  77. data/lib/backup/template.rb +46 -0
  78. data/lib/backup/utilities.rb +224 -0
  79. data/lib/backup/version.rb +5 -0
  80. data/templates/cli/archive +28 -0
  81. data/templates/cli/compressor/bzip2 +4 -0
  82. data/templates/cli/compressor/custom +7 -0
  83. data/templates/cli/compressor/gzip +4 -0
  84. data/templates/cli/config +123 -0
  85. data/templates/cli/databases/mongodb +15 -0
  86. data/templates/cli/databases/mysql +18 -0
  87. data/templates/cli/databases/openldap +24 -0
  88. data/templates/cli/databases/postgresql +16 -0
  89. data/templates/cli/databases/redis +16 -0
  90. data/templates/cli/databases/riak +17 -0
  91. data/templates/cli/databases/sqlite +11 -0
  92. data/templates/cli/encryptor/gpg +27 -0
  93. data/templates/cli/encryptor/openssl +9 -0
  94. data/templates/cli/model +26 -0
  95. data/templates/cli/notifier/zabbix +15 -0
  96. data/templates/cli/notifiers/campfire +12 -0
  97. data/templates/cli/notifiers/command +32 -0
  98. data/templates/cli/notifiers/datadog +57 -0
  99. data/templates/cli/notifiers/flowdock +16 -0
  100. data/templates/cli/notifiers/hipchat +16 -0
  101. data/templates/cli/notifiers/http_post +32 -0
  102. data/templates/cli/notifiers/mail +24 -0
  103. data/templates/cli/notifiers/nagios +13 -0
  104. data/templates/cli/notifiers/pagerduty +12 -0
  105. data/templates/cli/notifiers/prowl +11 -0
  106. data/templates/cli/notifiers/pushover +11 -0
  107. data/templates/cli/notifiers/ses +15 -0
  108. data/templates/cli/notifiers/slack +22 -0
  109. data/templates/cli/notifiers/twitter +13 -0
  110. data/templates/cli/splitter +7 -0
  111. data/templates/cli/storages/cloud_files +11 -0
  112. data/templates/cli/storages/dropbox +20 -0
  113. data/templates/cli/storages/ftp +13 -0
  114. data/templates/cli/storages/local +8 -0
  115. data/templates/cli/storages/qiniu +12 -0
  116. data/templates/cli/storages/rsync +17 -0
  117. data/templates/cli/storages/s3 +16 -0
  118. data/templates/cli/storages/scp +15 -0
  119. data/templates/cli/storages/sftp +15 -0
  120. data/templates/cli/syncers/cloud_files +22 -0
  121. data/templates/cli/syncers/rsync_local +20 -0
  122. data/templates/cli/syncers/rsync_pull +28 -0
  123. data/templates/cli/syncers/rsync_push +28 -0
  124. data/templates/cli/syncers/s3 +27 -0
  125. data/templates/general/links +3 -0
  126. data/templates/general/version.erb +2 -0
  127. data/templates/notifier/mail/failure.erb +16 -0
  128. data/templates/notifier/mail/success.erb +16 -0
  129. data/templates/notifier/mail/warning.erb +16 -0
  130. data/templates/storage/dropbox/authorization_url.erb +6 -0
  131. data/templates/storage/dropbox/authorized.erb +4 -0
  132. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  133. metadata +1077 -0
@@ -0,0 +1,116 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ class Logger
5
+ class Syslog
6
+ class Options
7
+ ##
8
+ # Enables logging to the system's Syslog compatible logger.
9
+ #
10
+ # This may also be enabled using +--syslog+ on the command line.
11
+ #
12
+ # If +--no-syslog+ is used on the command line, this will be
13
+ # disabled and any settings here will be ignored.
14
+ #
15
+ # @param [Boolean, nil]
16
+ # @return [Boolean, nil] Default: +false+
17
+ attr_reader :enabled
18
+
19
+ ##
20
+ # Specify the identification string to be used with Syslog.
21
+ #
22
+ # @param [String]
23
+ # @return [String] Default: 'backup'
24
+ attr_accessor :ident
25
+
26
+ ##
27
+ # Specify the options to be used with Syslog.
28
+ #
29
+ # See the Ruby Standard Library documentation for +Syslog+ for more info.
30
+ # http://rdoc.info/stdlib/syslog/Syslog.open
31
+ #
32
+ # Note that setting this to +nil+ will cause this to default
33
+ # to a setting of +Syslog::LOG_PID | Syslog::LOG_CONS+
34
+ #
35
+ # @param [Integer]
36
+ # @return [Integer] Default: +Syslog::LOG_PID+
37
+ attr_accessor :options
38
+
39
+ ##
40
+ # Specify the facility to be used with Syslog.
41
+ #
42
+ # See the Ruby Standard Library documentation for +Syslog+ for more info.
43
+ # http://rdoc.info/stdlib/syslog/Syslog.open
44
+ #
45
+ # Note that setting this to +nil+ will cause this to default
46
+ # to a setting of +Syslog::LOG_USER+
47
+ #
48
+ # @param [Integer]
49
+ # @return [Integer] Default: +Syslog::LOG_LOCAL0+
50
+ attr_accessor :facility
51
+
52
+ ##
53
+ # Specify the priority level to be used for +:info+ messages.
54
+ #
55
+ # See the Ruby Standard Library documentation for +Syslog+ for more info.
56
+ # http://rdoc.info/stdlib/syslog/Syslog.log
57
+ #
58
+ # @param [Integer]
59
+ # @return [Integer] Default: +Syslog::LOG_INFO+
60
+ attr_accessor :info
61
+
62
+ ##
63
+ # Specify the priority level to be used for +:warn+ messages.
64
+ #
65
+ # See the Ruby Standard Library documentation for +Syslog+ for more info.
66
+ # http://rdoc.info/stdlib/syslog/Syslog.log
67
+ #
68
+ # @param [Integer]
69
+ # @return [Integer] Default: +Syslog::LOG_WARNING+
70
+ attr_accessor :warn
71
+
72
+ ##
73
+ # Specify the priority level to be used for +:error+ messages.
74
+ #
75
+ # See the Ruby Standard Library documentation for +Syslog+ for more info.
76
+ # http://rdoc.info/stdlib/syslog/Syslog.log
77
+ #
78
+ # @param [Integer]
79
+ # @return [Integer] Default: +Syslog::LOG_ERR+
80
+ attr_accessor :error
81
+
82
+ def initialize
83
+ @enabled = false
84
+ @ident = 'backup'
85
+ @options = ::Syslog::LOG_PID
86
+ @facility = ::Syslog::LOG_LOCAL0
87
+ @info = ::Syslog::LOG_INFO
88
+ @warn = ::Syslog::LOG_WARNING
89
+ @error = ::Syslog::LOG_ERR
90
+ end
91
+
92
+ def enabled?
93
+ !!enabled
94
+ end
95
+
96
+ def enabled=(val)
97
+ @enabled = val unless enabled.nil?
98
+ end
99
+ end
100
+
101
+ def initialize(options)
102
+ @options = options
103
+ end
104
+
105
+ ##
106
+ # Message lines are sent without formatting (timestamp, level),
107
+ # since Syslog will provide it's own timestamp and priority.
108
+ def log(message)
109
+ level = @options.send(message.level)
110
+ ::Syslog.open(@options.ident, @options.options, @options.facility) do |s|
111
+ message.lines.each {|line| s.log(level, '%s', line) }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,479 @@
1
+ # encoding: utf-8
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 = /^#{ 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
+
274
+ rescue Interrupt
275
+ @interrupted = true
276
+ raise
277
+
278
+ rescue Exception => err
279
+ @exception = err
280
+
281
+ ensure
282
+ unless @interrupted
283
+ set_exit_status
284
+ @finished_at = Time.now.utc
285
+ log!(:finished)
286
+ after_hook
287
+ end
288
+ end
289
+
290
+ ##
291
+ # The duration of the backup process (in format: HH:MM:SS)
292
+ def duration
293
+ return unless finished_at
294
+ elapsed_time(started_at, finished_at)
295
+ end
296
+
297
+ private
298
+
299
+ ##
300
+ # Returns an array of procedures that will be performed if any
301
+ # Archives or Databases are configured for the model.
302
+ def procedures
303
+ return [] unless databases.any? || archives.any?
304
+
305
+ [lambda { prepare! }, databases, archives,
306
+ lambda { package! }, lambda { store! }, lambda { clean! }]
307
+ end
308
+
309
+ ##
310
+ # Clean any temporary files and/or package files left over
311
+ # from the last time this model/trigger was performed.
312
+ # Logs warnings if files exist and are cleaned.
313
+ def prepare!
314
+ Cleaner.prepare(self)
315
+ end
316
+
317
+ ##
318
+ # After all the databases and archives have been dumped and stored,
319
+ # these files will be bundled in to a .tar archive (uncompressed),
320
+ # which may be optionally Encrypted and/or Split into multiple "chunks".
321
+ # All information about this final archive is stored in the @package.
322
+ # Once complete, the temporary folder used during packaging is removed.
323
+ def package!
324
+ Packager.package!(self)
325
+ Cleaner.remove_packaging(self)
326
+ end
327
+
328
+ ##
329
+ # Attempts to use all configured Storages, even if some of them result in exceptions.
330
+ # Returns true or raises first encountered exception.
331
+ def store!
332
+ storage_results = storages.map do |storage|
333
+ begin
334
+ storage.perform!
335
+ rescue => ex
336
+ ex
337
+ end
338
+ end
339
+
340
+ first_exception, *other_exceptions = storage_results.select { |result| result.is_a? Exception }
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(/^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 = if exception
389
+ exception.is_a?(StandardError) ? 2 : 3
390
+ else
391
+ Logger.has_warnings? ? 1 : 0
392
+ end
393
+ end
394
+
395
+ ##
396
+ # Runs the +before+ hook.
397
+ # Any exception raised will be wrapped and re-raised, where it will be
398
+ # handled by #perform the same as an exception raised while performing
399
+ # the model's #procedures. Only difference is that an exception raised
400
+ # here will prevent any +after+ hook from being run.
401
+ def before_hook
402
+ return unless before
403
+
404
+ Logger.info 'Before Hook Starting...'
405
+ before.call
406
+ Logger.info 'Before Hook Finished.'
407
+
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
+
427
+ rescue Exception => err
428
+ fatal = !err.is_a?(StandardError)
429
+ ex = fatal ? FatalError : Error
430
+ Logger.error ex.wrap(err, 'After Hook Failed!')
431
+ # upgrade exit_status if needed
432
+ (@exit_status = fatal ? 3 : 2) unless exit_status == 3
433
+ end
434
+
435
+ ##
436
+ # Logs messages when the model starts and finishes.
437
+ #
438
+ # #exception will be set here if #exit_status is > 1,
439
+ # since log(:finished) is called before the +after+ hook.
440
+ def log!(action)
441
+ case action
442
+ when :started
443
+ Logger.info "Performing Backup for '#{ label } (#{ trigger })'!\n" +
444
+ "[ backup #{ VERSION } : #{ RUBY_DESCRIPTION } ]"
445
+
446
+ when :finished
447
+ if exit_status > 1
448
+ ex = exit_status == 2 ? Error : FatalError
449
+ err = ex.wrap(exception, "Backup for #{ label } (#{ trigger }) Failed!")
450
+ Logger.error err
451
+ Logger.error "\nBacktrace:\n\s\s" + err.backtrace.join("\n\s\s") + "\n\n"
452
+
453
+ Cleaner.warnings(self)
454
+ else
455
+ msg = "Backup for '#{ label } (#{ trigger })' "
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
+ '%02d:%02d:%02d' % [hours, minutes, seconds]
476
+ end
477
+
478
+ end
479
+ end