backup 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -4,6 +4,7 @@ require 'net/scp'
4
4
  module Backup
5
5
  module Storage
6
6
  class SCP < Base
7
+ class Error < Backup::Error; end
7
8
 
8
9
  ##
9
10
  # Server credentials
@@ -13,9 +14,8 @@ module Backup
13
14
  # Server IP Address and SCP port
14
15
  attr_accessor :ip, :port
15
16
 
16
- def initialize(model, storage_id = nil, &block)
17
+ def initialize(model, storage_id = nil)
17
18
  super
18
- instance_eval(&block) if block_given?
19
19
 
20
20
  @port ||= 22
21
21
  @path ||= 'backups'
@@ -55,8 +55,7 @@ module Backup
55
55
  end
56
56
  end
57
57
  unless errors.empty?
58
- raise Errors::Storage::SCP::SSHError,
59
- "Net::SSH reported the following errors:\n" +
58
+ raise Error, "Net::SSH reported the following errors:\n" +
60
59
  errors.join("\n")
61
60
  end
62
61
  end
@@ -13,9 +13,8 @@ module Backup
13
13
  # Server IP Address and SFTP port
14
14
  attr_accessor :ip, :port
15
15
 
16
- def initialize(model, storage_id = nil, &block)
16
+ def initialize(model, storage_id = nil)
17
17
  super
18
- instance_eval(&block) if block_given?
19
18
 
20
19
  @port ||= 22
21
20
  @path ||= 'backups'
@@ -25,7 +25,6 @@ module Backup
25
25
 
26
26
  load_defaults!
27
27
 
28
- @path ||= '~/backups'
29
28
  @mirror ||= false
30
29
  @directories = Array.new
31
30
  end
@@ -1,257 +1,178 @@
1
1
  # encoding: utf-8
2
2
 
3
- ##
4
- # Only load the Fog gem, along with the Parallel gem, when the
5
- # Backup::Syncer::Cloud class is loaded
6
- # Backup::Dependency.load('fog')
7
- # Backup::Dependency.load('parallel')
8
- require 'fog'
9
- require 'parallel'
10
-
11
3
  module Backup
12
4
  module Syncer
13
5
  module Cloud
14
- class Base < Syncer::Base
6
+ class Error < Backup::Error; end
15
7
 
16
- ##
17
- # Create a Mutex to synchronize certain parts of the code
18
- # in order to prevent race conditions or broken STDOUT.
8
+ class Base < Syncer::Base
19
9
  MUTEX = Mutex.new
20
10
 
21
11
  ##
22
- # Concurrency setting - defaults to false, but can be set to:
23
- # - :threads
24
- # - :processes
25
- attr_accessor :concurrency_type
12
+ # Number of threads to use for concurrency.
13
+ #
14
+ # Default: 0 (no concurrency)
15
+ attr_accessor :thread_count
26
16
 
27
17
  ##
28
- # Concurrency level - the number of threads or processors to use.
29
- # Defaults to 2.
30
- attr_accessor :concurrency_level
18
+ # Number of times to retry failed operations.
19
+ #
20
+ # Default: 10
21
+ attr_accessor :max_retries
31
22
 
32
23
  ##
33
- # Instantiates a new Cloud Syncer object for either
34
- # the Cloud::S3 or Cloud::CloudFiles Syncer.
24
+ # Time in seconds to pause before each retry.
35
25
  #
36
- # Pre-configured defaults specified in either
37
- # Configuration::Syncer::Cloud::S3 or
38
- # Configuration::Syncer::Cloud::CloudFiles
39
- # are set via a super() call to Syncer::Base.
40
- #
41
- # If not specified in the pre-configured defaults,
42
- # the Cloud specific defaults are set here before evaluating
43
- # any block provided in the user's configuration file.
44
- def initialize(syncer_id = nil)
26
+ # Default: 30
27
+ attr_accessor :retry_waitsec
28
+
29
+ def initialize(syncer_id = nil, &block)
45
30
  super
31
+ instance_eval(&block) if block_given?
32
+
33
+ @thread_count ||= 0
34
+ @max_retries ||= 10
35
+ @retry_waitsec ||= 30
46
36
 
47
- @path = path.sub(/^~\//, '')
48
- @concurrency_type ||= false
49
- @concurrency_level ||= 2
37
+ @path ||= 'backups'
38
+ @path = path.sub(/^\//, '')
50
39
  end
51
40
 
52
- ##
53
- # Performs the Sync operation
54
41
  def perform!
55
42
  log!(:started)
56
- Logger.info(
57
- "\s\sConcurrency: #{ @concurrency_type } Level: #{ @concurrency_level }"
58
- )
59
-
60
- @directories.each do |directory|
61
- SyncContext.new(
62
- File.expand_path(directory), repository_object, @path
63
- ).sync! @mirror, @concurrency_type, @concurrency_level
64
- end
65
-
43
+ @transfer_count = 0
44
+ @unchanged_count = 0
45
+ @skipped_count = 0
46
+ @orphans = thread_count > 0 ? Queue.new : []
47
+
48
+ directories.each {|dir| sync_directory(dir) }
49
+ orphans_result = process_orphans
50
+
51
+ Logger.info "\nSummary:"
52
+ Logger.info "\s\sTransferred Files: #{ @transfer_count }"
53
+ Logger.info "\s\s#{ orphans_result }"
54
+ Logger.info "\s\sUnchanged Files: #{ @unchanged_count }"
55
+ Logger.warn "\s\sSkipped Files: #{ @skipped_count }" if @skipped_count > 0
66
56
  log!(:finished)
67
57
  end
68
58
 
69
59
  private
70
60
 
71
- class SyncContext
72
- include Utilities::Helpers
73
-
74
- attr_reader :directory, :bucket, :path, :remote_base
75
-
76
- ##
77
- # Creates a new SyncContext object which handles a single directory
78
- # from the Syncer::Base @directories array.
79
- def initialize(directory, bucket, path)
80
- @directory, @bucket, @path = directory, bucket, path
81
- @remote_base = File.join(path, File.basename(directory))
82
- end
83
-
84
- ##
85
- # Performs the sync operation using the provided techniques
86
- # (mirroring/concurrency).
87
- def sync!(mirror = false, concurrency_type = false, concurrency_level = 2)
88
- block = Proc.new { |relative_path| sync_file relative_path, mirror }
61
+ def sync_directory(dir)
62
+ remote_base = File.join(path, File.basename(dir))
63
+ Logger.info "Gathering remote data for '#{ remote_base }'..."
64
+ remote_files = get_remote_files(remote_base)
65
+
66
+ Logger.info("Gathering local data for '#{ File.expand_path(dir) }'...")
67
+ local_files = LocalFile.find(dir)
68
+
69
+ relative_paths = (local_files.keys | remote_files.keys).sort
70
+ if relative_paths.empty?
71
+ Logger.info 'No local or remote files found'
72
+ else
73
+ Logger.info 'Syncing...'
74
+ sync_block = Proc.new do |relative_path|
75
+ local_file = local_files[relative_path]
76
+ remote_md5 = remote_files[relative_path]
77
+ remote_path = File.join(remote_base, relative_path)
78
+ sync_file(local_file, remote_path, remote_md5)
79
+ end
89
80
 
90
- case concurrency_type
91
- when FalseClass
92
- all_file_names.each(&block)
93
- when :threads
94
- Parallel.each all_file_names,
95
- :in_threads => concurrency_level, &block
96
- when :processes
97
- Parallel.each all_file_names,
98
- :in_processes => concurrency_level, &block
81
+ if thread_count > 0
82
+ sync_in_threads(relative_paths, sync_block)
99
83
  else
100
- raise Errors::Syncer::Cloud::ConfigurationError,
101
- "Unknown concurrency_type setting: #{ concurrency_type.inspect }"
84
+ relative_paths.each(&sync_block)
102
85
  end
103
86
  end
87
+ end
104
88
 
105
- private
106
-
107
- ##
108
- # Gathers all the relative paths to the local files
109
- # and merges them with the , removing
110
- # duplicate keys if any, and sorts the in alphabetical order.
111
- def all_file_names
112
- @all_file_names ||= (local_files.keys | remote_files.keys).sort
113
- end
114
-
115
- ##
116
- # Returns a Hash of local files, validated to ensure the path
117
- # does not contain invalid UTF-8 byte sequences.
118
- # The keys are the filesystem paths, relative to @directory.
119
- # The values are the LocalFile objects for that given file.
120
- def local_files
121
- @local_files ||= begin
122
- hash = {}
123
- local_hashes.lines.map do |line|
124
- LocalFile.new(@directory, line)
125
- end.compact.each do |file|
126
- hash.merge!(file.relative_path => file)
89
+ def sync_in_threads(relative_paths, sync_block)
90
+ queue = Queue.new
91
+ queue << relative_paths.shift until relative_paths.empty?
92
+ num_threads = [thread_count, queue.size].min
93
+ Logger.info "\s\sUsing #{ num_threads } Threads"
94
+ threads = num_threads.times.map do
95
+ Thread.new do
96
+ loop do
97
+ path = queue.shift(true) rescue nil
98
+ path ? sync_block.call(path) : break
127
99
  end
128
- hash
129
100
  end
130
101
  end
131
102
 
132
- ##
133
- # Returns a String of file paths and their md5 hashes.
134
- #
135
- # Utilities#run is not used here because this would produce too much
136
- # log output, and Pipeline does not support capturing output.
137
- def local_hashes
138
- Logger.info("\s\sGenerating checksums for '#{ @directory }'")
139
- cmd = "#{ utility(:find) } -L '#{ @directory }' -type f -print0 | " +
140
- "#{ utility(:xargs) } -0 #{ utility(:openssl) } md5 2> /dev/null"
141
- %x[#{ cmd }]
142
- end
143
-
144
- ##
145
- # Returns a Hash of remote files
146
- # The keys are the remote paths, relative to @remote_base
147
- # The values are the Fog file objects for that given file
148
- def remote_files
149
- @remote_files ||= begin
150
- hash = {}
151
- @bucket.files.all(:prefix => @remote_base).each do |file|
152
- hash.merge!(file.key.sub("#{ @remote_base }/", '') => file)
153
- end
154
- hash
103
+ # abort if any thread raises an exception
104
+ while threads.any?(&:alive?)
105
+ if threads.any? {|thr| thr.status.nil? }
106
+ threads.each(&:kill)
107
+ Thread.pass while threads.any?(&:alive?)
108
+ break
155
109
  end
110
+ sleep num_threads * 0.1
156
111
  end
112
+ threads.each(&:join)
113
+ end
157
114
 
158
- ##
159
- # Performs a sync operation on a file. When mirroring is enabled
160
- # and a local file has been removed since the last sync, it will also
161
- # remove it from the remote location. It will no upload files that
162
- # have not changed since the last sync. Checks are done using an md5
163
- # hash. If a file has changed, or has been newly added, the file will
164
- # be transferred/overwritten.
165
- def sync_file(relative_path, mirror)
166
- local_file = local_files[relative_path]
167
- remote_file = remote_files[relative_path]
168
- remote_path = File.join(@remote_base, relative_path)
169
-
170
- if local_file && File.exist?(local_file.path)
171
- unless remote_file && remote_file.etag == local_file.md5
172
- MUTEX.synchronize {
173
- Logger.info("\s\s[transferring] '#{ remote_path }'")
174
- }
175
- File.open(local_file.path, 'r') do |file|
176
- @bucket.files.create(
177
- :key => remote_path,
178
- :body => file
179
- )
180
- end
181
- else
182
- MUTEX.synchronize {
183
- Logger.info("\s\s[skipping] '#{ remote_path }'")
184
- }
185
- end
186
- elsif remote_file
187
- if mirror
188
- MUTEX.synchronize {
189
- Logger.info("\s\s[removing] '#{ remote_path }'")
190
- }
191
- remote_file.destroy
192
- else
193
- MUTEX.synchronize {
194
- Logger.info("\s\s[leaving] '#{ remote_path }'")
195
- }
115
+ # If an exception is raised in multiple threads, only the exception
116
+ # raised in the first thread that Thread#join is called on will be
117
+ # handled. So all exceptions are logged first with their details,
118
+ # then a generic exception is raised.
119
+ def sync_file(local_file, remote_path, remote_md5)
120
+ if local_file && File.exist?(local_file.path)
121
+ if local_file.md5 == remote_md5
122
+ MUTEX.synchronize { @unchanged_count += 1 }
123
+ else
124
+ Logger.info("\s\s[transferring] '#{ remote_path }'")
125
+ begin
126
+ cloud_io.upload(local_file.path, remote_path)
127
+ MUTEX.synchronize { @transfer_count += 1 }
128
+ rescue CloudIO::FileSizeError => err
129
+ MUTEX.synchronize { @skipped_count += 1 }
130
+ Logger.warn Error.wrap(err, "Skipping '#{ remote_path }'")
131
+ rescue => err
132
+ Logger.error(err)
133
+ raise Error, <<-EOS
134
+ Syncer Failed!
135
+ See the Retry [info] and [error] messages (if any)
136
+ for details on each failed operation.
137
+ EOS
196
138
  end
197
139
  end
140
+ elsif remote_md5
141
+ @orphans << remote_path
198
142
  end
199
- end # class SyncContext
200
-
201
- class LocalFile
202
- attr_reader :path, :relative_path, :md5
203
-
204
- ##
205
- # Return a new LocalFile object if it's valid.
206
- # Otherwise, log a warning and return nil.
207
- def self.new(*args)
208
- local_file = super(*args)
209
- if local_file.invalid?
210
- Logger.warn(
211
- "\s\s[skipping] #{ local_file.path }\n" +
212
- "\s\sPath Contains Invalid UTF-8 byte sequences"
213
- )
214
- return nil
215
- end
216
- local_file
217
- end
143
+ end
218
144
 
219
- ##
220
- # Creates a new LocalFile object using the given directory and line
221
- # from the md5 hash checkup. This object figures out the path,
222
- # relative_path and md5 hash for the file.
223
- def initialize(directory, line)
224
- @invalid = false
225
- @directory = sanitize(directory)
226
- line = sanitize(line).chomp
227
- @path = line.slice(4..-36)
228
- @md5 = line.slice(-32..-1)
229
- @relative_path = @path.sub(@directory + '/', '')
145
+ def process_orphans
146
+ if @orphans.empty?
147
+ return mirror ? 'Deleted Files: 0' : 'Orphaned Files: 0'
230
148
  end
231
149
 
232
- def invalid?
233
- @invalid
150
+ if @orphans.is_a?(Queue)
151
+ @orphans = @orphans.size.times.map { @orphans.shift }
234
152
  end
235
153
 
236
- private
237
-
238
- ##
239
- # Sanitize string and replace any invalid UTF-8 characters.
240
- # If replacements are made, flag the LocalFile object as invalid.
241
- def sanitize(str)
242
- str.each_char.map do |char|
243
- begin
244
- char if !!char.unpack('U')
245
- rescue
246
- @invalid = true
247
- "\xEF\xBF\xBD" # => "\uFFFD"
248
- end
249
- end.join
154
+ if mirror
155
+ Logger.info @orphans.map {|path|
156
+ "\s\s[removing] '#{ path }'"
157
+ }.join("\n")
158
+
159
+ begin
160
+ cloud_io.delete(@orphans)
161
+ "Deleted Files: #{ @orphans.count }"
162
+ rescue => err
163
+ Logger.warn Error.wrap(err, 'Delete Operation Failed')
164
+ "Attempted to Delete: #{ @orphans.count } " +
165
+ "(See log messages for actual results)"
166
+ end
167
+ else
168
+ Logger.info @orphans.map {|path|
169
+ "\s\s[orphaned] '#{ path }'"
170
+ }.join("\n")
171
+ "Orphaned Files: #{ @orphans.count }"
250
172
  end
173
+ end
251
174
 
252
- end # class LocalFile
253
-
254
- end # class Base < Syncer::Base
255
- end # module Cloud
175
+ end
176
+ end
256
177
  end
257
178
  end
@@ -1,77 +1,92 @@
1
1
  # encoding: utf-8
2
+ require 'backup/cloud_io/cloud_files'
2
3
 
3
4
  module Backup
4
5
  module Syncer
5
6
  module Cloud
6
7
  class CloudFiles < Base
8
+ class Error < Backup::Error; end
7
9
 
8
10
  ##
9
11
  # Rackspace CloudFiles Credentials
10
- attr_accessor :api_key, :username
12
+ attr_accessor :username, :api_key
11
13
 
12
14
  ##
13
15
  # Rackspace CloudFiles Container
14
16
  attr_accessor :container
15
17
 
16
18
  ##
17
- # Rackspace AuthURL allows you to connect
18
- # to a different Rackspace datacenter
19
- # - https://auth.api.rackspacecloud.com (Default: US)
20
- # - https://lon.auth.api.rackspacecloud.com (UK)
19
+ # Rackspace AuthURL (optional)
21
20
  attr_accessor :auth_url
22
21
 
23
22
  ##
24
- # Improve performance and avoid data transfer costs
25
- # by setting @servicenet to `true`
26
- # This only works if Backup runs on a Rackspace server
27
- attr_accessor :servicenet
23
+ # Rackspace Region (optional)
24
+ attr_accessor :region
28
25
 
29
26
  ##
30
- # Instantiates a new Cloud::CloudFiles Syncer.
31
- #
32
- # Pre-configured defaults specified in
33
- # Configuration::Syncer::Cloud::CloudFiles
34
- # are set via a super() call to Cloud::Base,
35
- # which in turn will invoke Syncer::Base.
36
- #
37
- # Once pre-configured defaults and Cloud specific defaults are set,
38
- # the block from the user's configuration file is evaluated.
39
- def initialize(syncer_id = nil, &block)
27
+ # Rackspace Service Net
28
+ # (LAN-based transfers to avoid charges and improve performance)
29
+ attr_accessor :servicenet
30
+
31
+ def initialize(syncer_id = nil)
40
32
  super
41
33
 
42
- instance_eval(&block) if block_given?
43
- @path = path.sub(/^\//, '')
34
+ @servicenet ||= false
35
+
36
+ check_configuration
44
37
  end
45
38
 
46
39
  private
47
40
 
48
- ##
49
- # Established and creates a new Fog storage object for CloudFiles.
50
- def connection
51
- @connection ||= Fog::Storage.new(
52
- :provider => provider,
53
- :rackspace_username => username,
54
- :rackspace_api_key => api_key,
55
- :rackspace_auth_url => auth_url,
56
- :rackspace_servicenet => servicenet
41
+ def cloud_io
42
+ @cloud_io ||= CloudIO::CloudFiles.new(
43
+ :username => username,
44
+ :api_key => api_key,
45
+ :auth_url => auth_url,
46
+ :region => region,
47
+ :servicenet => servicenet,
48
+ :container => container,
49
+ :max_retries => max_retries,
50
+ :retry_waitsec => retry_waitsec,
51
+ # Syncer can not use SLOs.
52
+ :segments_container => nil,
53
+ :segment_size => 0
57
54
  )
58
55
  end
59
56
 
60
- ##
61
- # Creates a new @repository_object (container).
62
- # Fetches it from Cloud Files if it already exists,
63
- # otherwise it will create it first and fetch use that instead.
64
- def repository_object
65
- @repository_object ||= connection.directories.get(container) ||
66
- connection.directories.create(:key => container)
57
+ def get_remote_files(remote_base)
58
+ hash = {}
59
+ cloud_io.objects(remote_base).each do |object|
60
+ relative_path = object.name.sub(remote_base + '/', '')
61
+ hash[relative_path] = object.hash
62
+ end
63
+ hash
67
64
  end
68
65
 
69
- ##
70
- # This is the provider that Fog uses for the Cloud Files
71
- def provider
72
- "Rackspace"
66
+ def check_configuration
67
+ required = %w{ username api_key container }
68
+ raise Error, <<-EOS if required.map {|name| send(name) }.any?(&:nil?)
69
+ Configuration Error
70
+ #{ required.map {|name| "##{ name }"}.join(', ') } are all required
71
+ EOS
73
72
  end
74
73
 
74
+ attr_deprecate :concurrency_type, :version => '3.7.0',
75
+ :message => 'Use #thread_count instead.',
76
+ :action => lambda {|klass, val|
77
+ if val == :threads
78
+ klass.thread_count = 2 unless klass.thread_count
79
+ else
80
+ klass.thread_count = 0
81
+ end
82
+ }
83
+
84
+ attr_deprecate :concurrency_level, :version => '3.7.0',
85
+ :message => 'Use #thread_count instead.',
86
+ :action => lambda {|klass, val|
87
+ klass.thread_count = val unless klass.thread_count == 0
88
+ }
89
+
75
90
  end # class Cloudfiles < Base
76
91
  end # module Cloud
77
92
  end