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.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +37 -0
- data/bin/backupii +5 -0
- data/bin/docker_test +24 -0
- data/lib/backup/archive.rb +171 -0
- data/lib/backup/binder.rb +23 -0
- data/lib/backup/cleaner.rb +114 -0
- data/lib/backup/cli.rb +376 -0
- data/lib/backup/cloud_io/base.rb +40 -0
- data/lib/backup/cloud_io/cloud_files.rb +301 -0
- data/lib/backup/cloud_io/s3.rb +256 -0
- data/lib/backup/compressor/base.rb +34 -0
- data/lib/backup/compressor/bzip2.rb +37 -0
- data/lib/backup/compressor/custom.rb +51 -0
- data/lib/backup/compressor/gzip.rb +76 -0
- data/lib/backup/config/dsl.rb +103 -0
- data/lib/backup/config/helpers.rb +139 -0
- data/lib/backup/config.rb +122 -0
- data/lib/backup/database/base.rb +89 -0
- data/lib/backup/database/mongodb.rb +189 -0
- data/lib/backup/database/mysql.rb +194 -0
- data/lib/backup/database/openldap.rb +97 -0
- data/lib/backup/database/postgresql.rb +134 -0
- data/lib/backup/database/redis.rb +179 -0
- data/lib/backup/database/riak.rb +82 -0
- data/lib/backup/database/sqlite.rb +57 -0
- data/lib/backup/encryptor/base.rb +29 -0
- data/lib/backup/encryptor/gpg.rb +745 -0
- data/lib/backup/encryptor/open_ssl.rb +76 -0
- data/lib/backup/errors.rb +55 -0
- data/lib/backup/logger/console.rb +50 -0
- data/lib/backup/logger/fog_adapter.rb +27 -0
- data/lib/backup/logger/logfile.rb +134 -0
- data/lib/backup/logger/syslog.rb +116 -0
- data/lib/backup/logger.rb +199 -0
- data/lib/backup/model.rb +478 -0
- data/lib/backup/notifier/base.rb +128 -0
- data/lib/backup/notifier/campfire.rb +63 -0
- data/lib/backup/notifier/command.rb +101 -0
- data/lib/backup/notifier/datadog.rb +107 -0
- data/lib/backup/notifier/flowdock.rb +101 -0
- data/lib/backup/notifier/hipchat.rb +118 -0
- data/lib/backup/notifier/http_post.rb +116 -0
- data/lib/backup/notifier/mail.rb +235 -0
- data/lib/backup/notifier/nagios.rb +67 -0
- data/lib/backup/notifier/pagerduty.rb +82 -0
- data/lib/backup/notifier/prowl.rb +70 -0
- data/lib/backup/notifier/pushover.rb +73 -0
- data/lib/backup/notifier/ses.rb +126 -0
- data/lib/backup/notifier/slack.rb +149 -0
- data/lib/backup/notifier/twitter.rb +57 -0
- data/lib/backup/notifier/zabbix.rb +62 -0
- data/lib/backup/package.rb +53 -0
- data/lib/backup/packager.rb +108 -0
- data/lib/backup/pipeline.rb +122 -0
- data/lib/backup/splitter.rb +75 -0
- data/lib/backup/storage/base.rb +72 -0
- data/lib/backup/storage/cloud_files.rb +158 -0
- data/lib/backup/storage/cycler.rb +73 -0
- data/lib/backup/storage/dropbox.rb +208 -0
- data/lib/backup/storage/ftp.rb +118 -0
- data/lib/backup/storage/local.rb +63 -0
- data/lib/backup/storage/qiniu.rb +68 -0
- data/lib/backup/storage/rsync.rb +251 -0
- data/lib/backup/storage/s3.rb +157 -0
- data/lib/backup/storage/scp.rb +67 -0
- data/lib/backup/storage/sftp.rb +82 -0
- data/lib/backup/syncer/base.rb +70 -0
- data/lib/backup/syncer/cloud/base.rb +180 -0
- data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
- data/lib/backup/syncer/cloud/local_file.rb +99 -0
- data/lib/backup/syncer/cloud/s3.rb +118 -0
- data/lib/backup/syncer/rsync/base.rb +55 -0
- data/lib/backup/syncer/rsync/local.rb +29 -0
- data/lib/backup/syncer/rsync/pull.rb +49 -0
- data/lib/backup/syncer/rsync/push.rb +206 -0
- data/lib/backup/template.rb +45 -0
- data/lib/backup/utilities.rb +235 -0
- data/lib/backup/version.rb +5 -0
- data/lib/backup.rb +141 -0
- data/templates/cli/archive +28 -0
- data/templates/cli/compressor/bzip2 +4 -0
- data/templates/cli/compressor/custom +7 -0
- data/templates/cli/compressor/gzip +4 -0
- data/templates/cli/config +123 -0
- data/templates/cli/databases/mongodb +15 -0
- data/templates/cli/databases/mysql +18 -0
- data/templates/cli/databases/openldap +24 -0
- data/templates/cli/databases/postgresql +16 -0
- data/templates/cli/databases/redis +16 -0
- data/templates/cli/databases/riak +17 -0
- data/templates/cli/databases/sqlite +11 -0
- data/templates/cli/encryptor/gpg +27 -0
- data/templates/cli/encryptor/openssl +9 -0
- data/templates/cli/model +26 -0
- data/templates/cli/notifier/zabbix +15 -0
- data/templates/cli/notifiers/campfire +12 -0
- data/templates/cli/notifiers/command +32 -0
- data/templates/cli/notifiers/datadog +57 -0
- data/templates/cli/notifiers/flowdock +16 -0
- data/templates/cli/notifiers/hipchat +16 -0
- data/templates/cli/notifiers/http_post +32 -0
- data/templates/cli/notifiers/mail +24 -0
- data/templates/cli/notifiers/nagios +13 -0
- data/templates/cli/notifiers/pagerduty +12 -0
- data/templates/cli/notifiers/prowl +11 -0
- data/templates/cli/notifiers/pushover +11 -0
- data/templates/cli/notifiers/ses +15 -0
- data/templates/cli/notifiers/slack +22 -0
- data/templates/cli/notifiers/twitter +13 -0
- data/templates/cli/splitter +7 -0
- data/templates/cli/storages/cloud_files +11 -0
- data/templates/cli/storages/dropbox +20 -0
- data/templates/cli/storages/ftp +13 -0
- data/templates/cli/storages/local +8 -0
- data/templates/cli/storages/qiniu +12 -0
- data/templates/cli/storages/rsync +17 -0
- data/templates/cli/storages/s3 +16 -0
- data/templates/cli/storages/scp +15 -0
- data/templates/cli/storages/sftp +15 -0
- data/templates/cli/syncers/cloud_files +22 -0
- data/templates/cli/syncers/rsync_local +20 -0
- data/templates/cli/syncers/rsync_pull +28 -0
- data/templates/cli/syncers/rsync_push +28 -0
- data/templates/cli/syncers/s3 +27 -0
- data/templates/general/links +3 -0
- data/templates/general/version.erb +2 -0
- data/templates/notifier/mail/failure.erb +16 -0
- data/templates/notifier/mail/success.erb +16 -0
- data/templates/notifier/mail/warning.erb +16 -0
- data/templates/storage/dropbox/authorization_url.erb +6 -0
- data/templates/storage/dropbox/authorized.erb +4 -0
- data/templates/storage/dropbox/cache_file_written.erb +10 -0
- metadata +507 -0
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module Storage
|
5
|
+
class RSync < Base
|
6
|
+
include Utilities::Helpers
|
7
|
+
|
8
|
+
##
|
9
|
+
# Mode of operation
|
10
|
+
#
|
11
|
+
# [:ssh (default)]
|
12
|
+
# Connects to the remote via SSH.
|
13
|
+
# Does not use an rsync daemon on the remote.
|
14
|
+
#
|
15
|
+
# [:ssh_daemon]
|
16
|
+
# Connects to the remote via SSH.
|
17
|
+
# Spawns a single-use daemon on the remote, which allows certain
|
18
|
+
# daemon features (like modules) to be used.
|
19
|
+
#
|
20
|
+
# [:rsync_daemon]
|
21
|
+
# Connects directly to an rsync daemon via TCP.
|
22
|
+
# Data transferred is not encrypted.
|
23
|
+
#
|
24
|
+
attr_accessor :mode
|
25
|
+
|
26
|
+
##
|
27
|
+
# Server Address
|
28
|
+
#
|
29
|
+
# If not specified, the storage operation will be local.
|
30
|
+
attr_accessor :host
|
31
|
+
|
32
|
+
##
|
33
|
+
# SSH or RSync port
|
34
|
+
#
|
35
|
+
# For `:ssh` or `:ssh_daemon` mode, this specifies the SSH port to use
|
36
|
+
# and defaults to 22.
|
37
|
+
#
|
38
|
+
# For `:rsync_daemon` mode, this specifies the TCP port to use
|
39
|
+
# and defaults to 873.
|
40
|
+
attr_accessor :port
|
41
|
+
|
42
|
+
##
|
43
|
+
# SSH User
|
44
|
+
#
|
45
|
+
# If the user running the backup is not the same user that needs to
|
46
|
+
# authenticate with the remote server, specify the user here.
|
47
|
+
#
|
48
|
+
# The user must have SSH keys setup for passphrase-less access to the
|
49
|
+
# remote. If the SSH User does not have passphrase-less keys, or no
|
50
|
+
# default keys in their `~/.ssh` directory, you will need to use the
|
51
|
+
# `-i` option in `:additional_ssh_options` to specify the
|
52
|
+
# passphrase-less key to use.
|
53
|
+
#
|
54
|
+
# Used only for `:ssh` and `:ssh_daemon` modes.
|
55
|
+
attr_accessor :ssh_user
|
56
|
+
|
57
|
+
##
|
58
|
+
# Additional SSH Options
|
59
|
+
#
|
60
|
+
# Used to supply a String or Array of options to be passed to the SSH
|
61
|
+
# command in `:ssh` and `:ssh_daemon` modes.
|
62
|
+
#
|
63
|
+
# For example, if you need to supply a specific SSH key for the
|
64
|
+
# `ssh_user`, you would set this to: "-i '/path/to/id_rsa'". Which would
|
65
|
+
# produce:
|
66
|
+
#
|
67
|
+
# rsync -e "ssh -p 22 -i '/path/to/id_rsa'"
|
68
|
+
#
|
69
|
+
# Arguments may be single-quoted, but should not contain any
|
70
|
+
# double-quotes.
|
71
|
+
#
|
72
|
+
# Used only for `:ssh` and `:ssh_daemon` modes.
|
73
|
+
attr_accessor :additional_ssh_options
|
74
|
+
|
75
|
+
##
|
76
|
+
# RSync User
|
77
|
+
#
|
78
|
+
# If the user running the backup is not the same user that needs to
|
79
|
+
# authenticate with the rsync daemon, specify the user here.
|
80
|
+
#
|
81
|
+
# Used only for `:ssh_daemon` and `:rsync_daemon` modes.
|
82
|
+
attr_accessor :rsync_user
|
83
|
+
|
84
|
+
##
|
85
|
+
# RSync Password
|
86
|
+
#
|
87
|
+
# If specified, Backup will write the password to a temporary file and
|
88
|
+
# use it with rsync's `--password-file` option for daemon authentication.
|
89
|
+
#
|
90
|
+
# Note that setting this will override `rsync_password_file`.
|
91
|
+
#
|
92
|
+
# Used only for `:ssh_daemon` and `:rsync_daemon` modes.
|
93
|
+
attr_accessor :rsync_password
|
94
|
+
|
95
|
+
##
|
96
|
+
# RSync Password File
|
97
|
+
#
|
98
|
+
# If specified, this path will be passed to rsync's `--password-file`
|
99
|
+
# option for daemon authentication.
|
100
|
+
#
|
101
|
+
# Used only for `:ssh_daemon` and `:rsync_daemon` modes.
|
102
|
+
attr_accessor :rsync_password_file
|
103
|
+
|
104
|
+
##
|
105
|
+
# Additional String or Array of options for the rsync cli
|
106
|
+
attr_accessor :additional_rsync_options
|
107
|
+
|
108
|
+
##
|
109
|
+
# Flag for compressing (only compresses for the transfer)
|
110
|
+
attr_accessor :compress
|
111
|
+
|
112
|
+
##
|
113
|
+
# Path to store the synced backup package file(s) to.
|
114
|
+
#
|
115
|
+
# If no +host+ is specified, then +path+ will be local, and the only
|
116
|
+
# other used option would be +additional_rsync_options+.
|
117
|
+
# +path+ will be expanded, so '~/my_path' will expand to '$HOME/my_path'.
|
118
|
+
#
|
119
|
+
# If a +host+ is specified, this will be a path on the host.
|
120
|
+
# If +mode+ is `:ssh` (default), then any relative path, or path starting
|
121
|
+
# with '~/' will be relative to the directory the ssh_user is logged
|
122
|
+
# into. For `:ssh_daemon` or `:rsync_daemon` modes, this would reference
|
123
|
+
# an rsync module/path.
|
124
|
+
#
|
125
|
+
# In :ssh_daemon and :rsync_daemon modes, +path+ (or path defined by
|
126
|
+
# your rsync module) must already exist.
|
127
|
+
#
|
128
|
+
# In :ssh mode or local operation (no +host+ specified), +path+ will
|
129
|
+
# be created if needed - either locally, or on the remote for :ssh mode.
|
130
|
+
attr_accessor :path
|
131
|
+
|
132
|
+
def initialize(model, storage_id = nil)
|
133
|
+
super
|
134
|
+
|
135
|
+
@mode ||= :ssh
|
136
|
+
@port ||= mode == :rsync_daemon ? 873 : 22
|
137
|
+
@compress ||= false
|
138
|
+
@path ||= "~/backups"
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def transfer!
|
144
|
+
write_password_file
|
145
|
+
create_remote_path
|
146
|
+
|
147
|
+
package.filenames.each do |filename|
|
148
|
+
src = "'#{File.join(Config.tmp_path, filename)}'"
|
149
|
+
dest = "#{host_options}'#{File.join(remote_path, filename)}'"
|
150
|
+
Logger.info "Syncing to #{dest}..."
|
151
|
+
run("#{rsync_command} #{src} #{dest}")
|
152
|
+
end
|
153
|
+
ensure
|
154
|
+
remove_password_file
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Other storages add an additional timestamp directory to this path.
|
159
|
+
# This is not desired here, since we need to transfer the package files
|
160
|
+
# to the same location each time.
|
161
|
+
def remote_path
|
162
|
+
@remote_path ||= begin
|
163
|
+
if host
|
164
|
+
path.sub(%r{^~/}, "").sub(%r{/$}, "")
|
165
|
+
else
|
166
|
+
File.expand_path(path)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# Runs a 'mkdir -p' command on the host (or locally) to ensure the
|
173
|
+
# dest_path exists. This is used because we're transferring a single
|
174
|
+
# file, and rsync won't attempt to create the intermediate directories.
|
175
|
+
#
|
176
|
+
# This is only applicable locally and in :ssh mode.
|
177
|
+
# In :ssh_daemon and :rsync_daemon modes the `path` would include a
|
178
|
+
# module name that must define a path on the remote that already exists.
|
179
|
+
def create_remote_path
|
180
|
+
if host
|
181
|
+
return unless mode == :ssh
|
182
|
+
|
183
|
+
run "#{utility(:ssh)} #{ssh_transport_args} #{host} " +
|
184
|
+
%("mkdir -p '#{remote_path}'")
|
185
|
+
else
|
186
|
+
FileUtils.mkdir_p(remote_path)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def host_options
|
191
|
+
@host_options ||= begin
|
192
|
+
if !host
|
193
|
+
""
|
194
|
+
elsif mode == :ssh
|
195
|
+
"#{host}:"
|
196
|
+
else
|
197
|
+
user = "#{rsync_user}@" if rsync_user
|
198
|
+
"#{user}#{host}::"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def rsync_command
|
204
|
+
@rsync_command ||= begin
|
205
|
+
cmd = utility(:rsync).dup << " --archive" <<
|
206
|
+
" #{Array(additional_rsync_options).join(" ")}".rstrip
|
207
|
+
cmd << compress_option << password_option << transport_options if host
|
208
|
+
cmd
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def compress_option
|
213
|
+
compress ? " --compress" : ""
|
214
|
+
end
|
215
|
+
|
216
|
+
def password_option
|
217
|
+
return "" if mode == :ssh
|
218
|
+
|
219
|
+
path = @password_file ? @password_file.path : rsync_password_file
|
220
|
+
path ? " --password-file='#{File.expand_path(path)}'" : ""
|
221
|
+
end
|
222
|
+
|
223
|
+
def transport_options
|
224
|
+
if mode == :rsync_daemon
|
225
|
+
" --port #{port}"
|
226
|
+
else
|
227
|
+
%( -e "#{utility(:ssh)} #{ssh_transport_args}")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def ssh_transport_args
|
232
|
+
args = "-p #{port} ".dup
|
233
|
+
args << "-l #{ssh_user} " if ssh_user
|
234
|
+
args << Array(additional_ssh_options).join(" ")
|
235
|
+
args.rstrip
|
236
|
+
end
|
237
|
+
|
238
|
+
def write_password_file
|
239
|
+
return unless host && rsync_password && mode != :ssh
|
240
|
+
|
241
|
+
@password_file = Tempfile.new("backup-rsync-password")
|
242
|
+
@password_file.write(rsync_password)
|
243
|
+
@password_file.close
|
244
|
+
end
|
245
|
+
|
246
|
+
def remove_password_file
|
247
|
+
@password_file.delete if @password_file
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "backup/cloud_io/s3"
|
4
|
+
|
5
|
+
module Backup
|
6
|
+
module Storage
|
7
|
+
class S3 < Base
|
8
|
+
include Storage::Cycler
|
9
|
+
class Error < Backup::Error; end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Amazon Simple Storage Service (S3) Credentials
|
13
|
+
attr_accessor :access_key_id, :secret_access_key, :use_iam_profile
|
14
|
+
|
15
|
+
##
|
16
|
+
# Amazon S3 bucket name
|
17
|
+
attr_accessor :bucket
|
18
|
+
|
19
|
+
##
|
20
|
+
# Region of the specified S3 bucket
|
21
|
+
attr_accessor :region
|
22
|
+
|
23
|
+
##
|
24
|
+
# Multipart chunk size, specified in MiB.
|
25
|
+
#
|
26
|
+
# Each package file larger than +chunk_size+
|
27
|
+
# will be uploaded using S3 Multipart Upload.
|
28
|
+
#
|
29
|
+
# Minimum: 5 (but may be disabled with 0)
|
30
|
+
# Maximum: 5120
|
31
|
+
# Default: 5
|
32
|
+
attr_accessor :chunk_size
|
33
|
+
|
34
|
+
##
|
35
|
+
# Number of times to retry failed operations.
|
36
|
+
#
|
37
|
+
# Default: 10
|
38
|
+
attr_accessor :max_retries
|
39
|
+
|
40
|
+
##
|
41
|
+
# Time in seconds to pause before each retry.
|
42
|
+
#
|
43
|
+
# Default: 30
|
44
|
+
attr_accessor :retry_waitsec
|
45
|
+
|
46
|
+
##
|
47
|
+
# Encryption algorithm to use for Amazon Server-Side Encryption
|
48
|
+
#
|
49
|
+
# Supported values:
|
50
|
+
#
|
51
|
+
# - :aes256
|
52
|
+
#
|
53
|
+
# Default: nil
|
54
|
+
attr_accessor :encryption
|
55
|
+
|
56
|
+
##
|
57
|
+
# Storage class to use for the S3 objects uploaded
|
58
|
+
#
|
59
|
+
# Supported values:
|
60
|
+
#
|
61
|
+
# - :standard (default)
|
62
|
+
# - :standard_ia
|
63
|
+
# - :reduced_redundancy
|
64
|
+
#
|
65
|
+
# Default: :standard
|
66
|
+
attr_accessor :storage_class
|
67
|
+
|
68
|
+
##
|
69
|
+
# Additional options to pass along to fog.
|
70
|
+
# e.g. Fog::Storage.new({ :provider => 'AWS' }.merge(fog_options))
|
71
|
+
attr_accessor :fog_options
|
72
|
+
|
73
|
+
def initialize(model, storage_id = nil)
|
74
|
+
super
|
75
|
+
|
76
|
+
@chunk_size ||= 5 # MiB
|
77
|
+
@max_retries ||= 10
|
78
|
+
@retry_waitsec ||= 30
|
79
|
+
@path ||= "backups"
|
80
|
+
@storage_class ||= :standard
|
81
|
+
|
82
|
+
@path = @path.sub(%r{^/}, "")
|
83
|
+
|
84
|
+
check_configuration
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def cloud_io
|
90
|
+
@cloud_io ||= CloudIO::S3.new(
|
91
|
+
access_key_id: access_key_id,
|
92
|
+
secret_access_key: secret_access_key,
|
93
|
+
use_iam_profile: use_iam_profile,
|
94
|
+
region: region,
|
95
|
+
bucket: bucket,
|
96
|
+
encryption: encryption,
|
97
|
+
storage_class: storage_class,
|
98
|
+
max_retries: max_retries,
|
99
|
+
retry_waitsec: retry_waitsec,
|
100
|
+
chunk_size: chunk_size,
|
101
|
+
fog_options: fog_options
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def transfer!
|
106
|
+
package.filenames.each do |filename|
|
107
|
+
src = File.join(Config.tmp_path, filename)
|
108
|
+
dest = File.join(remote_path, filename)
|
109
|
+
Logger.info "Storing '#{bucket}/#{dest}'..."
|
110
|
+
cloud_io.upload(src, dest)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Called by the Cycler.
|
115
|
+
# Any error raised will be logged as a warning.
|
116
|
+
def remove!(package)
|
117
|
+
Logger.info "Removing backup package dated #{package.time}..."
|
118
|
+
|
119
|
+
remote_path = remote_path_for(package)
|
120
|
+
objects = cloud_io.objects(remote_path)
|
121
|
+
|
122
|
+
raise Error, "Package at '#{remote_path}' not found" if objects.empty?
|
123
|
+
|
124
|
+
cloud_io.delete(objects)
|
125
|
+
end
|
126
|
+
|
127
|
+
def check_configuration
|
128
|
+
required =
|
129
|
+
if use_iam_profile
|
130
|
+
%w[bucket]
|
131
|
+
else
|
132
|
+
%w[access_key_id secret_access_key bucket]
|
133
|
+
end
|
134
|
+
raise Error, <<-EOS if required.map { |name| send(name) }.any?(&:nil?)
|
135
|
+
Configuration Error
|
136
|
+
#{required.map { |name| "##{name}" }.join(", ")} are all required
|
137
|
+
EOS
|
138
|
+
|
139
|
+
raise Error, <<-EOS if chunk_size > 0 && !chunk_size.between?(5, 5120)
|
140
|
+
Configuration Error
|
141
|
+
#chunk_size must be between 5 and 5120 (or 0 to disable multipart)
|
142
|
+
EOS
|
143
|
+
|
144
|
+
raise Error, <<-EOS if encryption && encryption.to_s.upcase != "AES256"
|
145
|
+
Configuration Error
|
146
|
+
#encryption must be :aes256 or nil
|
147
|
+
EOS
|
148
|
+
|
149
|
+
classes = %w[STANDARD STANDARD_IA REDUCED_REDUNDANCY]
|
150
|
+
raise Error, <<-EOS unless classes.include?(storage_class.to_s.upcase)
|
151
|
+
Configuration Error
|
152
|
+
#storage_class must be :standard or :standard_ia or :reduced_redundancy
|
153
|
+
EOS
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/scp"
|
4
|
+
|
5
|
+
module Backup
|
6
|
+
module Storage
|
7
|
+
class SCP < Base
|
8
|
+
include Storage::Cycler
|
9
|
+
class Error < Backup::Error; end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Server credentials
|
13
|
+
attr_accessor :username, :password, :ssh_options
|
14
|
+
|
15
|
+
##
|
16
|
+
# Server IP Address and SCP port
|
17
|
+
attr_accessor :ip, :port
|
18
|
+
|
19
|
+
def initialize(model, storage_id = nil)
|
20
|
+
super
|
21
|
+
|
22
|
+
@port ||= 22
|
23
|
+
@path ||= "backups".dup
|
24
|
+
@ssh_options ||= {}
|
25
|
+
path.sub!(%r{^~/}, "")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def connection
|
31
|
+
Net::SSH.start(
|
32
|
+
ip, username, { password: password, port: port }.merge(ssh_options)
|
33
|
+
) { |ssh| yield ssh }
|
34
|
+
end
|
35
|
+
|
36
|
+
def transfer!
|
37
|
+
connection do |ssh|
|
38
|
+
ssh.exec!("mkdir -p '#{remote_path}'")
|
39
|
+
|
40
|
+
package.filenames.each do |filename|
|
41
|
+
src = File.join(Config.tmp_path, filename)
|
42
|
+
dest = File.join(remote_path, filename)
|
43
|
+
Logger.info "Storing '#{ip}:#{dest}'..."
|
44
|
+
ssh.scp.upload!(src, dest)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Called by the Cycler.
|
50
|
+
# Any error raised will be logged as a warning.
|
51
|
+
def remove!(package)
|
52
|
+
Logger.info "Removing backup package dated #{package.time}..."
|
53
|
+
|
54
|
+
errors = []
|
55
|
+
connection do |ssh|
|
56
|
+
ssh.exec!("rm -r '#{remote_path_for(package)}'") do |_, stream, data|
|
57
|
+
errors << data if stream == :stderr
|
58
|
+
end
|
59
|
+
end
|
60
|
+
unless errors.empty?
|
61
|
+
raise Error, "Net::SSH reported the following errors:\n" +
|
62
|
+
errors.join("\n")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/sftp"
|
4
|
+
|
5
|
+
module Backup
|
6
|
+
module Storage
|
7
|
+
class SFTP < Base
|
8
|
+
include Storage::Cycler
|
9
|
+
|
10
|
+
##
|
11
|
+
# Server credentials
|
12
|
+
attr_accessor :username, :password, :ssh_options
|
13
|
+
|
14
|
+
##
|
15
|
+
# Server IP Address and SFTP port
|
16
|
+
attr_accessor :ip, :port
|
17
|
+
|
18
|
+
def initialize(model, storage_id = nil)
|
19
|
+
super
|
20
|
+
|
21
|
+
@ssh_options ||= {}
|
22
|
+
@port ||= 22
|
23
|
+
@path ||= "backups".dup
|
24
|
+
path.sub!(%r{^~/}, "")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def connection
|
30
|
+
Net::SFTP.start(
|
31
|
+
ip, username, { password: password, port: port }.merge(ssh_options)
|
32
|
+
) { |sftp| yield sftp }
|
33
|
+
end
|
34
|
+
|
35
|
+
def transfer!
|
36
|
+
connection do |sftp|
|
37
|
+
create_remote_path(sftp)
|
38
|
+
|
39
|
+
package.filenames.each do |filename|
|
40
|
+
src = File.join(Config.tmp_path, filename)
|
41
|
+
dest = File.join(remote_path, filename)
|
42
|
+
Logger.info "Storing '#{ip}:#{dest}'..."
|
43
|
+
sftp.upload!(src, dest)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Called by the Cycler.
|
49
|
+
# Any error raised will be logged as a warning.
|
50
|
+
def remove!(package)
|
51
|
+
Logger.info "Removing backup package dated #{package.time}..."
|
52
|
+
|
53
|
+
remote_path = remote_path_for(package)
|
54
|
+
connection do |sftp|
|
55
|
+
package.filenames.each do |filename|
|
56
|
+
sftp.remove!(File.join(remote_path, filename))
|
57
|
+
end
|
58
|
+
|
59
|
+
sftp.rmdir!(remote_path)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Creates (if they don't exist yet) all the directories on the remote
|
65
|
+
# server in order to upload the backup file. Net::SFTP does not support
|
66
|
+
# paths to directories that don't yet exist when creating new
|
67
|
+
# directories. Instead, we split the parts up in to an array (for each
|
68
|
+
# '/') and loop through that to create the directories one by one.
|
69
|
+
# Net::SFTP raises an exception when the directory it's trying to create
|
70
|
+
# already exists, so we have rescue it
|
71
|
+
def create_remote_path(sftp)
|
72
|
+
path_parts = []
|
73
|
+
remote_path.split("/").each do |path_part|
|
74
|
+
path_parts << path_part
|
75
|
+
begin
|
76
|
+
sftp.mkdir!(path_parts.join("/"))
|
77
|
+
rescue Net::SFTP::StatusException; end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module Syncer
|
5
|
+
class Base
|
6
|
+
include Utilities::Helpers
|
7
|
+
include Config::Helpers
|
8
|
+
|
9
|
+
##
|
10
|
+
# Path to store the synced files/directories to
|
11
|
+
attr_accessor :path
|
12
|
+
|
13
|
+
##
|
14
|
+
# Flag for mirroring the files/directories
|
15
|
+
attr_accessor :mirror
|
16
|
+
|
17
|
+
##
|
18
|
+
# Optional user-defined identifier to differentiate multiple syncers
|
19
|
+
# defined within a single backup model. Currently this is only used
|
20
|
+
# in the log messages.
|
21
|
+
attr_reader :syncer_id
|
22
|
+
|
23
|
+
attr_reader :excludes
|
24
|
+
|
25
|
+
def initialize(syncer_id = nil)
|
26
|
+
@syncer_id = syncer_id
|
27
|
+
|
28
|
+
load_defaults!
|
29
|
+
|
30
|
+
@mirror ||= false
|
31
|
+
@directories ||= []
|
32
|
+
@excludes ||= []
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Syntactical suger for the DSL for adding directories
|
37
|
+
def directories(&block)
|
38
|
+
return @directories unless block_given?
|
39
|
+
|
40
|
+
instance_eval(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def add(path)
|
44
|
+
directories << path
|
45
|
+
end
|
46
|
+
|
47
|
+
# For Cloud Syncers, +pattern+ can be a string (with shell-style
|
48
|
+
# wildcards) or a regex.
|
49
|
+
# For RSync, each +pattern+ will be passed to rsync's --exclude option.
|
50
|
+
def exclude(pattern)
|
51
|
+
excludes << pattern
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def syncer_name
|
57
|
+
@syncer_name ||= self.class.to_s.sub("Backup::", "") +
|
58
|
+
(syncer_id ? " (#{syncer_id})" : "")
|
59
|
+
end
|
60
|
+
|
61
|
+
def log!(action)
|
62
|
+
msg = case action
|
63
|
+
when :started then "Started..."
|
64
|
+
when :finished then "Finished!"
|
65
|
+
end
|
66
|
+
Logger.info "#{syncer_name} #{msg}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|