backup 3.6.0 → 3.7.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/lib/backup.rb +14 -4
  4. data/lib/backup/archive.rb +3 -2
  5. data/lib/backup/cleaner.rb +4 -2
  6. data/lib/backup/cli.rb +7 -5
  7. data/lib/backup/cloud_io/base.rb +41 -0
  8. data/lib/backup/cloud_io/cloud_files.rb +296 -0
  9. data/lib/backup/cloud_io/s3.rb +252 -0
  10. data/lib/backup/compressor/gzip.rb +2 -1
  11. data/lib/backup/config.rb +13 -5
  12. data/lib/backup/configuration.rb +1 -1
  13. data/lib/backup/configuration/helpers.rb +3 -1
  14. data/lib/backup/database/base.rb +3 -1
  15. data/lib/backup/database/mongodb.rb +2 -2
  16. data/lib/backup/database/mysql.rb +2 -2
  17. data/lib/backup/database/postgresql.rb +12 -2
  18. data/lib/backup/database/redis.rb +3 -2
  19. data/lib/backup/encryptor/gpg.rb +8 -10
  20. data/lib/backup/errors.rb +39 -70
  21. data/lib/backup/logger.rb +7 -2
  22. data/lib/backup/logger/fog_adapter.rb +30 -0
  23. data/lib/backup/model.rb +32 -14
  24. data/lib/backup/notifier/base.rb +4 -3
  25. data/lib/backup/notifier/campfire.rb +0 -1
  26. data/lib/backup/notifier/http_post.rb +122 -0
  27. data/lib/backup/notifier/mail.rb +38 -0
  28. data/lib/backup/notifier/nagios.rb +69 -0
  29. data/lib/backup/notifier/prowl.rb +0 -1
  30. data/lib/backup/notifier/pushover.rb +0 -1
  31. data/lib/backup/package.rb +5 -0
  32. data/lib/backup/packager.rb +3 -2
  33. data/lib/backup/pipeline.rb +4 -2
  34. data/lib/backup/storage/base.rb +2 -1
  35. data/lib/backup/storage/cloud_files.rb +151 -0
  36. data/lib/backup/storage/cycler.rb +4 -2
  37. data/lib/backup/storage/dropbox.rb +20 -16
  38. data/lib/backup/storage/ftp.rb +1 -2
  39. data/lib/backup/storage/local.rb +3 -3
  40. data/lib/backup/storage/ninefold.rb +3 -4
  41. data/lib/backup/storage/rsync.rb +1 -2
  42. data/lib/backup/storage/s3.rb +49 -158
  43. data/lib/backup/storage/scp.rb +3 -4
  44. data/lib/backup/storage/sftp.rb +1 -2
  45. data/lib/backup/syncer/base.rb +0 -1
  46. data/lib/backup/syncer/cloud/base.rb +129 -208
  47. data/lib/backup/syncer/cloud/cloud_files.rb +56 -41
  48. data/lib/backup/syncer/cloud/local_file.rb +93 -0
  49. data/lib/backup/syncer/cloud/s3.rb +78 -31
  50. data/lib/backup/syncer/rsync/base.rb +7 -0
  51. data/lib/backup/syncer/rsync/local.rb +0 -5
  52. data/lib/backup/syncer/rsync/push.rb +1 -2
  53. data/lib/backup/utilities.rb +18 -15
  54. data/lib/backup/version.rb +1 -1
  55. data/templates/cli/notifier/http_post +35 -0
  56. data/templates/cli/notifier/nagios +13 -0
  57. data/templates/cli/storage/cloud_files +8 -17
  58. data/templates/cli/storage/s3 +3 -10
  59. data/templates/cli/syncer/cloud_files +3 -31
  60. data/templates/cli/syncer/s3 +3 -27
  61. data/templates/notifier/mail/failure.erb +6 -1
  62. data/templates/notifier/mail/success.erb +6 -1
  63. data/templates/notifier/mail/warning.erb +6 -1
  64. metadata +37 -42
  65. data/lib/backup/storage/cloudfiles.rb +0 -68
@@ -3,6 +3,8 @@
3
3
  module Backup
4
4
  module Storage
5
5
  module Cycler
6
+ class Error < Backup::Error; end
7
+
6
8
  class << self
7
9
 
8
10
  ##
@@ -37,9 +39,9 @@ module Backup
37
39
  def remove_packages!
38
40
  @packages_to_remove.each do |pkg|
39
41
  begin
40
- @storage.send(:remove!, pkg)
42
+ @storage.send(:remove!, pkg) unless pkg.no_cycle
41
43
  rescue => err
42
- Logger.warn Errors::Storage::CyclerError.wrap(err, <<-EOS)
44
+ Logger.warn Error.wrap(err, <<-EOS)
43
45
  There was a problem removing the following package:
44
46
  Trigger: #{pkg.trigger} :: Dated: #{pkg.time}
45
47
  Package included the following #{ pkg.filenames.count } file(s):
@@ -4,6 +4,7 @@ require 'dropbox_sdk'
4
4
  module Backup
5
5
  module Storage
6
6
  class Dropbox < Base
7
+ class Error < Backup::Error; end
7
8
 
8
9
  ##
9
10
  # Dropbox API credentials
@@ -21,23 +22,26 @@ module Backup
21
22
  attr_accessor :chunk_size
22
23
 
23
24
  ##
24
- # Number of times to retry a failed chunk.
25
- attr_accessor :chunk_retries
25
+ # Number of times to retry failed operations.
26
+ #
27
+ # Default: 10
28
+ attr_accessor :max_retries
26
29
 
27
30
  ##
28
- # Seconds to wait between chunk retries.
31
+ # Time in seconds to pause before each retry.
32
+ #
33
+ # Default: 30
29
34
  attr_accessor :retry_waitsec
30
35
 
31
36
  ##
32
37
  # Creates a new instance of the storage object
33
- def initialize(model, storage_id = nil, &block)
38
+ def initialize(model, storage_id = nil)
34
39
  super
35
- instance_eval(&block) if block_given?
36
40
 
37
41
  @path ||= 'backups'
38
42
  @access_type ||= :app_folder
39
43
  @chunk_size ||= 4 # MiB
40
- @chunk_retries ||= 10
44
+ @max_retries ||= 10
41
45
  @retry_waitsec ||= 30
42
46
  path.sub!(/^\//, '')
43
47
  end
@@ -66,7 +70,7 @@ module Backup
66
70
  @connection = DropboxClient.new(session, access_type)
67
71
 
68
72
  rescue => err
69
- raise Errors::Storage::Dropbox::ConnectionError.wrap(err)
73
+ raise Error.wrap(err, 'Authorization Failed')
70
74
  end
71
75
 
72
76
  ##
@@ -79,7 +83,7 @@ module Backup
79
83
  Logger.info "Session data loaded from cache!"
80
84
 
81
85
  rescue => err
82
- Logger.warn Errors::Storage::Dropbox::CacheError.wrap(err, <<-EOS)
86
+ Logger.warn Error.wrap(err, <<-EOS)
83
87
  Could not read session data from cache.
84
88
  Cache data might be corrupt.
85
89
  EOS
@@ -114,7 +118,7 @@ module Backup
114
118
  end
115
119
 
116
120
  rescue => err
117
- raise Errors::Storage::Dropbox::TransferError.wrap(err, 'Upload Failed!')
121
+ raise Error.wrap(err, 'Upload Failed!')
118
122
  end
119
123
 
120
124
  # Timeout::Error is not a StandardError under ruby-1.8.7
@@ -124,10 +128,9 @@ module Backup
124
128
  yield
125
129
  rescue StandardError, Timeout::Error => err
126
130
  retries += 1
127
- raise if retries > chunk_retries
131
+ raise if retries > max_retries
128
132
 
129
- Logger.info Errors::Storage::Dropbox::TransferError.
130
- wrap(err, "Retry ##{ retries } of #{ chunk_retries }.")
133
+ Logger.info Error.wrap(err, "Retry ##{ retries } of #{ max_retries }.")
131
134
  sleep(retry_waitsec)
132
135
  retry
133
136
  end
@@ -186,16 +189,17 @@ module Backup
186
189
 
187
190
  session
188
191
 
189
- rescue => err
190
- raise Errors::Storage::Dropbox::AuthenticationError.wrap(
191
- err, 'Could not create or authenticate a new session'
192
- )
192
+ rescue => err
193
+ raise Error.wrap(err, 'Could not create or authenticate a new session')
193
194
  end
194
195
 
195
196
  attr_deprecate :email, :version => '3.0.17'
196
197
  attr_deprecate :password, :version => '3.0.17'
197
198
  attr_deprecate :timeout, :version => '3.0.21'
198
199
 
200
+ attr_deprecate :chunk_retries, :version => '3.7.0',
201
+ :message => 'Use #max_retries instead.',
202
+ :action => lambda {|klass, val| klass.max_retries = val }
199
203
  end
200
204
  end
201
205
  end
@@ -17,9 +17,8 @@ module Backup
17
17
  # use passive mode?
18
18
  attr_accessor :passive_mode
19
19
 
20
- def initialize(model, storage_id = nil, &block)
20
+ def initialize(model, storage_id = nil)
21
21
  super
22
- instance_eval(&block) if block_given?
23
22
 
24
23
  @port ||= 21
25
24
  @path ||= 'backups'
@@ -3,10 +3,10 @@
3
3
  module Backup
4
4
  module Storage
5
5
  class Local < Base
6
+ class Error < Backup::Error; end
6
7
 
7
- def initialize(model, storage_id = nil, &block)
8
+ def initialize(model, storage_id = nil)
8
9
  super
9
- instance_eval(&block) if block_given?
10
10
 
11
11
  @path ||= '~/backups'
12
12
  end
@@ -47,7 +47,7 @@ module Backup
47
47
  if self == model.storages.last
48
48
  true
49
49
  else
50
- Logger.warn Errors::Storage::Local::TransferError.new(<<-EOS)
50
+ Logger.warn Error.new(<<-EOS)
51
51
  Local File Copy Warning!
52
52
  The final backup file(s) for '#{ model.label }' (#{ model.trigger })
53
53
  will be *copied* to '#{ remote_path }'
@@ -4,14 +4,14 @@ require 'fog'
4
4
  module Backup
5
5
  module Storage
6
6
  class Ninefold < Base
7
+ class Error < Backup::Error; end
7
8
 
8
9
  ##
9
10
  # Ninefold Credentials
10
11
  attr_accessor :storage_token, :storage_secret
11
12
 
12
- def initialize(model, storage_id = nil, &block)
13
+ def initialize(model, storage_id = nil)
13
14
  super
14
- instance_eval(&block) if block_given?
15
15
 
16
16
  @path ||= 'backups'
17
17
  path.sub!(/^\//, '')
@@ -58,8 +58,7 @@ module Backup
58
58
  remote_path = remote_path_for(package)
59
59
  directory = directory_for(remote_path)
60
60
 
61
- raise Errors::Storage::Ninefold::NotFoundError,
62
- "Directory at '#{ remote_path }' not found" unless directory
61
+ raise Error, "Directory at '#{ remote_path }' not found" unless directory
63
62
 
64
63
  package.filenames.each do |filename|
65
64
  file = directory.files.get(filename)
@@ -132,9 +132,8 @@ module Backup
132
132
  # will also store the files directly in the +path+ given.
133
133
  attr_accessor :path
134
134
 
135
- def initialize(model, storage_id = nil, &block)
135
+ def initialize(model, storage_id = nil)
136
136
  super
137
- instance_eval(&block) if block_given?
138
137
 
139
138
  @mode ||= :ssh
140
139
  @port ||= mode == :rsync_daemon ? 873 : 22
@@ -1,11 +1,10 @@
1
1
  # encoding: utf-8
2
- require 'fog'
3
- require 'base64'
4
- require 'digest/md5'
2
+ require 'backup/cloud_io/s3'
5
3
 
6
4
  module Backup
7
5
  module Storage
8
6
  class S3 < Base
7
+ class Error < Backup::Error; end
9
8
 
10
9
  ##
11
10
  # Amazon Simple Storage Service (S3) Credentials
@@ -20,32 +19,19 @@ module Backup
20
19
  attr_accessor :region
21
20
 
22
21
  ##
23
- # Chunk size, specified in MiB, for S3 Multipart Upload.
22
+ # Multipart chunk size, specified in MiB.
24
23
  #
25
- # Each backup package file that is greater than +chunk_size+
26
- # will be uploaded using AWS' Multipart Upload.
24
+ # Each package file larger than +chunk_size+
25
+ # will be uploaded using S3 Multipart Upload.
27
26
  #
28
- # Package files less than or equal to +chunk_size+ will be
29
- # uploaded via a single PUT request.
30
- #
31
- # Minimum allowed: 5 (but may be disabled with 0)
27
+ # Minimum: 5 (but may be disabled with 0)
28
+ # Maximum: 5120
32
29
  # Default: 5
33
30
  attr_accessor :chunk_size
34
31
 
35
32
  ##
36
33
  # Number of times to retry failed operations.
37
34
  #
38
- # The retry count is reset when the failing operation succeeds,
39
- # so each operation that fails will be retried this number of times.
40
- # Once a single failed operation exceeds +max_retries+, the entire
41
- # storage operation will fail.
42
- #
43
- # Operations that may fail and be retried include:
44
- # - Multipart initiation requests.
45
- # - Each multipart upload of +chunk_size+. (retries the chunk)
46
- # - Multipart upload completion requests.
47
- # - Each file uploaded not using multipart upload. (retries the file)
48
- #
49
35
  # Default: 10
50
36
  attr_accessor :max_retries
51
37
 
@@ -62,8 +48,6 @@ module Backup
62
48
  #
63
49
  # - :aes256
64
50
  #
65
- # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html
66
- #
67
51
  # Default: nil
68
52
  attr_accessor :encryption
69
53
 
@@ -75,14 +59,11 @@ module Backup
75
59
  # - :standard (default)
76
60
  # - :reduced_redundancy
77
61
  #
78
- # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/SetStoClsOfObjUploaded.html
79
- #
80
62
  # Default: :standard
81
63
  attr_accessor :storage_class
82
64
 
83
- def initialize(model, storage_id = nil, &block)
65
+ def initialize(model, storage_id = nil)
84
66
  super
85
- instance_eval(&block) if block_given?
86
67
 
87
68
  @chunk_size ||= 5 # MiB
88
69
  @max_retries ||= 10
@@ -90,21 +71,24 @@ module Backup
90
71
  @path ||= 'backups'
91
72
  @storage_class ||= :standard
92
73
  path.sub!(/^\//, '')
74
+
75
+ check_configuration
93
76
  end
94
77
 
95
78
  private
96
79
 
97
- def connection
98
- @connection ||= begin
99
- conn = Fog::Storage.new(
100
- :provider => 'AWS',
101
- :aws_access_key_id => access_key_id,
102
- :aws_secret_access_key => secret_access_key,
103
- :region => region
104
- )
105
- conn.sync_clock
106
- conn
107
- end
80
+ def cloud_io
81
+ @cloud_io ||= CloudIO::S3.new(
82
+ :access_key_id => access_key_id,
83
+ :secret_access_key => secret_access_key,
84
+ :region => region,
85
+ :bucket => bucket,
86
+ :encryption => encryption,
87
+ :storage_class => storage_class,
88
+ :max_retries => max_retries,
89
+ :retry_waitsec => retry_waitsec,
90
+ :chunk_size => chunk_size
91
+ )
108
92
  end
109
93
 
110
94
  def transfer!
@@ -112,7 +96,7 @@ module Backup
112
96
  src = File.join(Config.tmp_path, filename)
113
97
  dest = File.join(remote_path, filename)
114
98
  Logger.info "Storing '#{ bucket }/#{ dest }'..."
115
- Uploader.new(self, connection, src, dest).run
99
+ cloud_io.upload(src, dest)
116
100
  end
117
101
  end
118
102
 
@@ -122,129 +106,36 @@ module Backup
122
106
  Logger.info "Removing backup package dated #{ package.time }..."
123
107
 
124
108
  remote_path = remote_path_for(package)
125
- resp = connection.get_bucket(bucket, :prefix => remote_path)
126
- keys = resp.body['Contents'].map {|entry| entry['Key'] }
109
+ objects = cloud_io.objects(remote_path)
127
110
 
128
- raise Errors::Storage::S3::NotFoundError,
129
- "Package at '#{ remote_path }' not found" if keys.empty?
111
+ raise Error, "Package at '#{ remote_path }' not found" if objects.empty?
130
112
 
131
- connection.delete_multiple_objects(bucket, keys)
113
+ cloud_io.delete(objects)
132
114
  end
133
115
 
134
- class Uploader
135
- attr_reader :connection, :bucket, :chunk_size, :max_retries,
136
- :retry_waitsec, :storage_class, :encryption,
137
- :src, :dest, :upload_id, :parts
138
-
139
- def initialize(storage, connection, src, dest)
140
- @connection = connection
141
- @bucket = storage.bucket
142
- @chunk_size = storage.chunk_size * 1024**2
143
- @max_retries = storage.max_retries
144
- @retry_waitsec = storage.retry_waitsec
145
- @encryption = storage.encryption
146
- @storage_class = storage.storage_class
147
- @src = src
148
- @dest = dest
149
- @parts = []
150
- end
151
-
152
- def run
153
- if chunk_size > 0 && File.size(src) > chunk_size
154
- initiate_multipart
155
- upload_parts
156
- complete_multipart
157
- else
158
- upload
159
- end
160
- rescue => err
161
- raise error_with(err, 'Upload Failed!')
162
- end
163
-
164
- private
165
-
166
- def upload
167
- md5 = Base64.encode64(Digest::MD5.file(src).digest).chomp
168
- options = headers.merge('Content-MD5' => md5)
169
- with_retries do
170
- File.open(src, 'r') do |file|
171
- connection.put_object(bucket, dest, file, options)
172
- end
173
- end
174
- end
175
-
176
- def initiate_multipart
177
- with_retries do
178
- resp = connection.initiate_multipart_upload(bucket, dest, headers)
179
- @upload_id = resp.body['UploadId']
180
- end
181
- end
182
-
183
- def upload_parts
184
- File.open(src, 'r') do |file|
185
- part_number = 0
186
- while data = file.read(chunk_size)
187
- part_number += 1
188
- md5 = Base64.encode64(Digest::MD5.digest(data)).chomp
189
- with_retries do
190
- resp = connection.upload_part(
191
- bucket, dest, upload_id, part_number, data,
192
- { 'Content-MD5' => md5 }
193
- )
194
- parts << resp.headers['ETag']
195
- end
196
- end
197
- end
198
- end
199
-
200
- def headers
201
- headers = {}
202
-
203
- val = encryption.to_s.upcase
204
- headers.merge!(
205
- { 'x-amz-server-side-encryption' => val }
206
- ) unless val.empty?
207
-
208
- val = storage_class.to_s.upcase
209
- headers.merge!(
210
- { 'x-amz-storage-class' => val }
211
- ) unless val.empty? || val == 'STANDARD'
212
-
213
- headers
214
- end
215
-
216
- def complete_multipart
217
- with_retries do
218
- connection.complete_multipart_upload(bucket, dest, upload_id, parts)
219
- end
220
- end
221
-
222
- def with_retries
223
- retries = 0
224
- begin
225
- yield
226
- rescue => err
227
- retries += 1
228
- raise if retries > max_retries
229
-
230
- Logger.info error_with(err, "Retry ##{ retries } of #{ max_retries }.")
231
- sleep(retry_waitsec)
232
- retry
233
- end
234
- end
235
-
236
- def error_with(err, msg)
237
- if err.is_a? Excon::Errors::HTTPStatusError
238
- Errors::Storage::S3::UploaderError.new(<<-EOS)
239
- #{ msg }
240
- Reason: #{ err.class }
241
- response => #{ err.response.inspect }
242
- EOS
243
- else
244
- Errors::Storage::S3::UploaderError.wrap(err, msg)
245
- end
246
- end
247
- end # class Uploader
116
+ def check_configuration
117
+ required = %w{ access_key_id secret_access_key bucket }
118
+ raise Error, <<-EOS if required.map {|name| send(name) }.any?(&:nil?)
119
+ Configuration Error
120
+ #{ required.map {|name| "##{ name }"}.join(', ') } are all required
121
+ EOS
122
+
123
+ raise Error, <<-EOS if chunk_size > 0 && !chunk_size.between?(5, 5120)
124
+ Configuration Error
125
+ #chunk_size must be between 5 and 5120 (or 0 to disable multipart)
126
+ EOS
127
+
128
+ raise Error, <<-EOS if encryption && encryption.to_s.upcase != 'AES256'
129
+ Configuration Error
130
+ #encryption must be :aes256 or nil
131
+ EOS
132
+
133
+ classes = ['STANDARD', 'REDUCED_REDUNDANCY']
134
+ raise Error, <<-EOS unless classes.include?(storage_class.to_s.upcase)
135
+ Configuration Error
136
+ #storage_class must be :standard or :reduced_redundancy
137
+ EOS
138
+ end
248
139
 
249
140
  end
250
141
  end