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,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Backup
|
7
|
+
module Notifier
|
8
|
+
class Slack < Base
|
9
|
+
##
|
10
|
+
# The incoming webhook url
|
11
|
+
attr_accessor :webhook_url
|
12
|
+
|
13
|
+
##
|
14
|
+
# The channel to send messages to
|
15
|
+
attr_accessor :channel
|
16
|
+
|
17
|
+
##
|
18
|
+
# The username to display along with the notification
|
19
|
+
attr_accessor :username
|
20
|
+
|
21
|
+
##
|
22
|
+
# The emoji icon to display along with the notification
|
23
|
+
#
|
24
|
+
# See http://www.emoji-cheat-sheet.com for a list of icons.
|
25
|
+
#
|
26
|
+
# Default: :floppy_disk:
|
27
|
+
attr_accessor :icon_emoji
|
28
|
+
|
29
|
+
##
|
30
|
+
# Array of statuses for which the log file should be attached.
|
31
|
+
#
|
32
|
+
# Available statuses are: `:success`, `:warning` and `:failure`.
|
33
|
+
# Default: [:warning, :failure]
|
34
|
+
attr_accessor :send_log_on
|
35
|
+
|
36
|
+
def initialize(model, &block)
|
37
|
+
super
|
38
|
+
instance_eval(&block) if block_given?
|
39
|
+
|
40
|
+
@send_log_on ||= [:warning, :failure]
|
41
|
+
@icon_emoji ||= ":floppy_disk:"
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
##
|
47
|
+
# Notify the user of the backup operation results.
|
48
|
+
#
|
49
|
+
# `status` indicates one of the following:
|
50
|
+
#
|
51
|
+
# `:success`
|
52
|
+
# : The backup completed successfully.
|
53
|
+
# : Notification will be sent if `on_success` is `true`.
|
54
|
+
#
|
55
|
+
# `:warning`
|
56
|
+
# : The backup completed successfully, but warnings were logged.
|
57
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
58
|
+
#
|
59
|
+
# `:failure`
|
60
|
+
# : The backup operation failed.
|
61
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
62
|
+
#
|
63
|
+
def notify!(status)
|
64
|
+
data = {
|
65
|
+
text: message.call(model, status: status_data_for(status)),
|
66
|
+
attachments: [attachment(status)]
|
67
|
+
}
|
68
|
+
[:channel, :username, :icon_emoji].each do |param|
|
69
|
+
val = send(param)
|
70
|
+
data.merge!(param => val) if val
|
71
|
+
end
|
72
|
+
|
73
|
+
options = {
|
74
|
+
headers: { "Content-Type" => "application/x-www-form-urlencoded" },
|
75
|
+
body: URI.encode_www_form(payload: JSON.dump(data))
|
76
|
+
}
|
77
|
+
options[:expects] = 200 # raise error if unsuccessful
|
78
|
+
Excon.post(uri, options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def attachment(status)
|
82
|
+
{
|
83
|
+
fallback: "#{title(status)} - Job: #{model.label} (#{model.trigger})",
|
84
|
+
text: title(status),
|
85
|
+
color: color(status),
|
86
|
+
fields: [
|
87
|
+
{
|
88
|
+
title: "Job",
|
89
|
+
value: "#{model.label} (#{model.trigger})",
|
90
|
+
short: false
|
91
|
+
},
|
92
|
+
{
|
93
|
+
title: "Started",
|
94
|
+
value: model.started_at,
|
95
|
+
short: true
|
96
|
+
},
|
97
|
+
{
|
98
|
+
title: "Finished",
|
99
|
+
value: model.finished_at,
|
100
|
+
short: true
|
101
|
+
},
|
102
|
+
{
|
103
|
+
title: "Duration",
|
104
|
+
value: model.duration,
|
105
|
+
short: true
|
106
|
+
},
|
107
|
+
{
|
108
|
+
title: "Version",
|
109
|
+
value: "Backup v#{Backup::VERSION}\nRuby: #{RUBY_DESCRIPTION}",
|
110
|
+
short: false
|
111
|
+
},
|
112
|
+
log_field(status)
|
113
|
+
].compact
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def log_field(status)
|
118
|
+
send_log = send_log_on.include?(status)
|
119
|
+
return unless send_log
|
120
|
+
|
121
|
+
{
|
122
|
+
title: "Detailed Backup Log",
|
123
|
+
value: Logger.messages.map(&:formatted_lines).flatten.join("\n"),
|
124
|
+
short: false
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def color(status)
|
129
|
+
case status
|
130
|
+
when :success then "good"
|
131
|
+
when :failure then "danger"
|
132
|
+
when :warning then "warning"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def title(status)
|
137
|
+
case status
|
138
|
+
when :success then "Backup Completed Successfully!"
|
139
|
+
when :failure then "Backup Failed!"
|
140
|
+
when :warning then "Backup Completed Successfully (with Warnings)!"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def uri
|
145
|
+
@uri ||= webhook_url
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "twitter"
|
4
|
+
|
5
|
+
module Backup
|
6
|
+
module Notifier
|
7
|
+
class Twitter < Base
|
8
|
+
##
|
9
|
+
# Twitter consumer key credentials
|
10
|
+
attr_accessor :consumer_key, :consumer_secret
|
11
|
+
|
12
|
+
##
|
13
|
+
# OAuth credentials
|
14
|
+
attr_accessor :oauth_token, :oauth_token_secret
|
15
|
+
|
16
|
+
def initialize(model, &block)
|
17
|
+
super
|
18
|
+
instance_eval(&block) if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
##
|
24
|
+
# Notify the user of the backup operation results.
|
25
|
+
#
|
26
|
+
# `status` indicates one of the following:
|
27
|
+
#
|
28
|
+
# `:success`
|
29
|
+
# : The backup completed successfully.
|
30
|
+
# : Notification will be sent if `on_success` is `true`.
|
31
|
+
#
|
32
|
+
# `:warning`
|
33
|
+
# : The backup completed successfully, but warnings were logged.
|
34
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
35
|
+
#
|
36
|
+
# `:failure`
|
37
|
+
# : The backup operation failed.
|
38
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
39
|
+
#
|
40
|
+
def notify!(status)
|
41
|
+
send_message(message.call(model, status: status_data_for(status)))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Twitter::Client will raise an error if unsuccessful.
|
45
|
+
def send_message(message)
|
46
|
+
client = ::Twitter::REST::Client.new do |config|
|
47
|
+
config.consumer_key = @consumer_key
|
48
|
+
config.consumer_secret = @consumer_secret
|
49
|
+
config.access_token = @oauth_token
|
50
|
+
config.access_token_secret = @oauth_token_secret
|
51
|
+
end
|
52
|
+
|
53
|
+
client.update(message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module Notifier
|
5
|
+
class Zabbix < Base
|
6
|
+
attr_accessor :zabbix_host
|
7
|
+
|
8
|
+
attr_accessor :zabbix_port
|
9
|
+
|
10
|
+
attr_accessor :service_name
|
11
|
+
|
12
|
+
attr_accessor :service_host
|
13
|
+
|
14
|
+
attr_accessor :item_key
|
15
|
+
|
16
|
+
def initialize(model, &block)
|
17
|
+
super
|
18
|
+
instance_eval(&block) if block_given?
|
19
|
+
|
20
|
+
@zabbix_host ||= Config.hostname
|
21
|
+
@zabbix_port ||= 10_051
|
22
|
+
@service_name ||= "Backup #{model.trigger}"
|
23
|
+
@service_host ||= Config.hostname
|
24
|
+
@item_key ||= "backup_status"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
##
|
30
|
+
# Notify the user of the backup operation results.
|
31
|
+
#
|
32
|
+
# `status` indicates one of the following:
|
33
|
+
#
|
34
|
+
# `:success`
|
35
|
+
# : The backup completed successfully.
|
36
|
+
# : Notification will be sent if `on_success` is `true`.
|
37
|
+
#
|
38
|
+
# `:warning`
|
39
|
+
# : The backup completed successfully, but warnings were logged.
|
40
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
41
|
+
#
|
42
|
+
# `:failure`
|
43
|
+
# : The backup operation failed.
|
44
|
+
# : Notification will be sent if `on_warning` or `on_success` is `true`.
|
45
|
+
#
|
46
|
+
def notify!(status)
|
47
|
+
send_message(message.call(model, status: status_data_for(status)))
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_message(message)
|
51
|
+
msg = [service_host, service_name, model.exit_status, message].join("\t")
|
52
|
+
cmd = utility(:zabbix_sender).to_s +
|
53
|
+
" -z '#{zabbix_host}'" \
|
54
|
+
" -p '#{zabbix_port}'" \
|
55
|
+
" -s #{service_host}" \
|
56
|
+
" -k #{item_key}" \
|
57
|
+
" -o '#{msg}'"
|
58
|
+
run("echo '#{msg}' | #{cmd}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
class Package
|
5
|
+
##
|
6
|
+
# The time when the backup initiated (in format: 2011.02.20.03.29.59)
|
7
|
+
attr_accessor :time
|
8
|
+
|
9
|
+
##
|
10
|
+
# The trigger which initiated the backup process
|
11
|
+
attr_reader :trigger
|
12
|
+
|
13
|
+
##
|
14
|
+
# Extension for the final archive file(s)
|
15
|
+
attr_accessor :extension
|
16
|
+
|
17
|
+
##
|
18
|
+
# Set by the Splitter if the final archive was "chunked"
|
19
|
+
attr_accessor :chunk_suffixes
|
20
|
+
|
21
|
+
##
|
22
|
+
# If true, the Cycler will not attempt to remove the package when Cycling.
|
23
|
+
attr_accessor :no_cycle
|
24
|
+
|
25
|
+
##
|
26
|
+
# The version of Backup used to create the package
|
27
|
+
attr_reader :version
|
28
|
+
|
29
|
+
def initialize(model)
|
30
|
+
@trigger = model.trigger.dup
|
31
|
+
@extension = "tar".dup
|
32
|
+
@chunk_suffixes = []
|
33
|
+
@no_cycle = false
|
34
|
+
@version = VERSION
|
35
|
+
end
|
36
|
+
|
37
|
+
def filenames
|
38
|
+
if chunk_suffixes.empty?
|
39
|
+
[basename]
|
40
|
+
else
|
41
|
+
chunk_suffixes.map { |suffix| "#{basename}-#{suffix}" }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def basename
|
46
|
+
"#{trigger}.#{extension}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def time_as_object
|
50
|
+
Time.strptime(time, "%Y.%m.%d.%H.%M.%S")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module Packager
|
5
|
+
class Error < Backup::Error; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
include Utilities::Helpers
|
9
|
+
|
10
|
+
##
|
11
|
+
# Build the final package for the backup model.
|
12
|
+
def package!(model)
|
13
|
+
@package = model.package
|
14
|
+
@encryptor = model.encryptor
|
15
|
+
@splitter = model.splitter
|
16
|
+
@pipeline = Pipeline.new
|
17
|
+
|
18
|
+
Logger.info "Packaging the backup files..."
|
19
|
+
procedure.call
|
20
|
+
|
21
|
+
if @pipeline.success?
|
22
|
+
Logger.info "Packaging Complete!"
|
23
|
+
else
|
24
|
+
raise Error, "Failed to Create Backup Package\n" +
|
25
|
+
@pipeline.error_messages
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
##
|
32
|
+
# Builds a chain of nested Procs which adds each command to a Pipeline
|
33
|
+
# needed to package the final command to package the backup.
|
34
|
+
# This is done so that the Encryptor and Splitter have the ability
|
35
|
+
# to perform actions before and after the final command is executed.
|
36
|
+
# No Encryptors currently utilize this, however the Splitter does.
|
37
|
+
def procedure
|
38
|
+
stack = []
|
39
|
+
|
40
|
+
##
|
41
|
+
# Initial `tar` command to package the temporary backup folder.
|
42
|
+
# The command's output will then be either piped to the Encryptor
|
43
|
+
# or the Splitter (if no Encryptor), or through `cat` into the final
|
44
|
+
# output file if neither are configured.
|
45
|
+
@pipeline.add(
|
46
|
+
"#{utility(:tar)} -cf - " \
|
47
|
+
"-C '#{Config.tmp_path}' '#{@package.trigger}'",
|
48
|
+
tar_success_codes
|
49
|
+
)
|
50
|
+
|
51
|
+
##
|
52
|
+
# If an Encryptor was configured, it will be called first
|
53
|
+
# to add the encryption utility command to be piped through,
|
54
|
+
# and amend the final package extension.
|
55
|
+
# It's output will then be either piped into a Splitter,
|
56
|
+
# or through `cat` into the final output file.
|
57
|
+
if @encryptor
|
58
|
+
stack << lambda do
|
59
|
+
@encryptor.encrypt_with do |command, ext|
|
60
|
+
@pipeline << command
|
61
|
+
@package.extension << ext
|
62
|
+
stack.shift.call
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# If a Splitter was configured, the `split` utility command will be
|
69
|
+
# added to the Pipeline to split the final output into multiple files.
|
70
|
+
# Once the Proc executing the Pipeline has completed and returns back
|
71
|
+
# to the Splitter, it will check the final output files to determine
|
72
|
+
# if the backup was indeed split.
|
73
|
+
# If so, it will set the package's chunk_suffixes. If not, it will
|
74
|
+
# remove the '-aa' suffix from the only file created by `split`.
|
75
|
+
#
|
76
|
+
# If no Splitter was configured, the final file output will be
|
77
|
+
# piped through `cat` into the final output file.
|
78
|
+
stack <<
|
79
|
+
if @splitter
|
80
|
+
lambda do
|
81
|
+
@splitter.split_with do |command|
|
82
|
+
@pipeline << command
|
83
|
+
stack.shift.call
|
84
|
+
end
|
85
|
+
end
|
86
|
+
else
|
87
|
+
lambda do
|
88
|
+
outfile = File.join(Config.tmp_path, @package.basename)
|
89
|
+
@pipeline << "#{utility(:cat)} > #{outfile}"
|
90
|
+
stack.shift.call
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Last Proc to be called runs the Pipeline the procedure built.
|
96
|
+
# Once complete, the call stack will unwind back through the
|
97
|
+
# preceeding Procs in the stack (if any)
|
98
|
+
stack << -> { @pipeline.run }
|
99
|
+
|
100
|
+
stack.shift
|
101
|
+
end
|
102
|
+
|
103
|
+
def tar_success_codes
|
104
|
+
gnu_tar? ? [0, 1] : [0]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
class Pipeline
|
5
|
+
class Error < Backup::Error; end
|
6
|
+
|
7
|
+
include Utilities::Helpers
|
8
|
+
|
9
|
+
attr_reader :stderr, :errors
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@commands = []
|
13
|
+
@success_codes = []
|
14
|
+
@errors = []
|
15
|
+
@stderr = ""
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Adds a command to be executed in the pipeline.
|
20
|
+
# Each command will be run in the order in which it was added,
|
21
|
+
# with it's output being piped to the next command.
|
22
|
+
#
|
23
|
+
# +success_codes+ must be an Array of Integer exit codes that will
|
24
|
+
# be considered successful for the +command+.
|
25
|
+
def add(command, success_codes)
|
26
|
+
@commands << command
|
27
|
+
@success_codes << success_codes
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Commands added using this method will only be considered successful
|
32
|
+
# if their exit status is 0.
|
33
|
+
#
|
34
|
+
# Use #add if successful exit status codes need to be specified.
|
35
|
+
def <<(command)
|
36
|
+
add(command, [0])
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Runs the command line from `#pipeline` and collects STDOUT/STDERR.
|
41
|
+
# STDOUT is then parsed to determine the exit status of each command.
|
42
|
+
# For each command with a non-zero exit status, a SystemCallError is
|
43
|
+
# created and added to @errors. All STDERR output is set in @stderr.
|
44
|
+
#
|
45
|
+
# Note that there is no accumulated STDOUT from the commands themselves.
|
46
|
+
# Also, the last command should not attempt to write to STDOUT.
|
47
|
+
# Any output on STDOUT from the final command will be sent to STDERR.
|
48
|
+
# This in itself will not cause #run to fail, but will log warnings
|
49
|
+
# when all commands exit with non-zero status.
|
50
|
+
#
|
51
|
+
# Use `#success?` to determine if all commands in the pipeline succeeded. If
|
52
|
+
# `#success?` returns `false`, use `#error_messages` to get an error report.
|
53
|
+
def run
|
54
|
+
Open4.popen4(pipeline) do |_pid, _stdin, stdout, stderr|
|
55
|
+
pipestatus = stdout.read.delete("\n").split(":").sort
|
56
|
+
pipestatus.each do |status|
|
57
|
+
index, exitstatus = status.split("|").map(&:to_i)
|
58
|
+
next if @success_codes[index].include?(exitstatus)
|
59
|
+
|
60
|
+
command = command_name(@commands[index])
|
61
|
+
@errors << SystemCallError.new(
|
62
|
+
"'#{command}' returned exit code: #{exitstatus}", exitstatus
|
63
|
+
)
|
64
|
+
end
|
65
|
+
@stderr = stderr.read.strip
|
66
|
+
end
|
67
|
+
Logger.warn(stderr_messages) if success? && stderr_messages
|
68
|
+
rescue Exception => err
|
69
|
+
raise Error.wrap(err, "Pipeline failed to execute")
|
70
|
+
end
|
71
|
+
|
72
|
+
def success?
|
73
|
+
@errors.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Returns a multi-line String, reporting all STDERR messages received
|
78
|
+
# from the commands in the pipeline (if any), along with the SystemCallError
|
79
|
+
# (Errno) message for each command which had a non-zero exit status.
|
80
|
+
def error_messages
|
81
|
+
@error_messages ||= (stderr_messages || "") +
|
82
|
+
"The following system errors were returned:\n" +
|
83
|
+
@errors.map { |err| "#{err.class}: #{err.message}" }.join("\n")
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
##
|
89
|
+
# Each command is added as part of the pipeline, grouped with an `echo`
|
90
|
+
# command to pass along the command's index in @commands and it's exit
|
91
|
+
# status. The command's STDERR is redirected to FD#4, and the `echo` command
|
92
|
+
# to report the "index|exit status" is redirected to FD#3. Each command's
|
93
|
+
# STDOUT will be connected to the STDIN of the next subshell. The entire
|
94
|
+
# pipeline is run within a container group, which redirects FD#3 to STDOUT
|
95
|
+
# and FD#4 to STDERR so these can be collected. FD#1 is redirected to STDERR
|
96
|
+
# so that any output from the final command on STDOUT will generate
|
97
|
+
# warnings, since the final command should not attempt to write to STDOUT,
|
98
|
+
# as this would interfere with collecting the exit statuses.
|
99
|
+
#
|
100
|
+
# There is no guarantee as to the order of this output, which is why the
|
101
|
+
# command's index in @commands is passed along with it's exit status. And,
|
102
|
+
# if multiple commands output messages on STDERR, those messages may be
|
103
|
+
# interleaved. Interleaving of the "index|exit status" outputs should not be
|
104
|
+
# an issue, given the small byte size of the data being written.
|
105
|
+
def pipeline
|
106
|
+
parts = []
|
107
|
+
@commands.each_with_index do |command, index|
|
108
|
+
parts << %({ #{command} 2>&4 ; echo "#{index}|$?:" >&3 ; })
|
109
|
+
end
|
110
|
+
%({ #{parts.join(" | ")} } 3>&1 1>&2 4>&2)
|
111
|
+
end
|
112
|
+
|
113
|
+
def stderr_messages
|
114
|
+
@stderr_messages ||= @stderr.empty? ? false : <<-EOS.gsub(%r{^ +}, " ")
|
115
|
+
Pipeline STDERR Messages:
|
116
|
+
(Note: may be interleaved if multiple commands returned error messages)
|
117
|
+
|
118
|
+
#{@stderr}
|
119
|
+
EOS
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
class Splitter
|
5
|
+
include Utilities::Helpers
|
6
|
+
|
7
|
+
attr_reader :package, :chunk_size, :suffix_length
|
8
|
+
|
9
|
+
def initialize(model, chunk_size, suffix_length)
|
10
|
+
@package = model.package
|
11
|
+
@chunk_size = chunk_size
|
12
|
+
@suffix_length = suffix_length
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# This is called as part of the procedure used to build the final
|
17
|
+
# backup package file(s). It yields it's portion of the command line
|
18
|
+
# for this procedure, which will split the data being piped into it
|
19
|
+
# into multiple files, based on the @chunk_size, using a suffix length as
|
20
|
+
# specified by @suffix_length.
|
21
|
+
# Once the packaging procedure is complete, it will return and
|
22
|
+
# @package.chunk_suffixes will be set based on the resulting files.
|
23
|
+
def split_with
|
24
|
+
Logger.info "Splitter configured with a chunk size of #{chunk_size}MB " \
|
25
|
+
"and suffix length of #{suffix_length}."
|
26
|
+
yield split_command
|
27
|
+
after_packaging
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
##
|
33
|
+
# The `split` command reads from $stdin and will store it's output in
|
34
|
+
# multiple files, based on @chunk_size and @suffix_length, using the full
|
35
|
+
# path to the final @package.basename, plus a '-' separator as the `prefix`.
|
36
|
+
def split_command
|
37
|
+
"#{utility(:split)} -a #{suffix_length} -b #{chunk_size}m - " \
|
38
|
+
"'#{File.join(Config.tmp_path, package.basename + "-")}'"
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Finds the resulting files from the packaging procedure
|
43
|
+
# and stores an Array of suffixes used in @package.chunk_suffixes.
|
44
|
+
# If the @chunk_size was never reached and only one file
|
45
|
+
# was written, that file will be suffixed with '-aa' (or -a; -aaa; etc
|
46
|
+
# depending upon suffix_length). In which case, it will simply
|
47
|
+
# remove the suffix from the filename.
|
48
|
+
def after_packaging
|
49
|
+
suffixes = chunk_suffixes
|
50
|
+
first_suffix = "a" * suffix_length
|
51
|
+
if suffixes == [first_suffix]
|
52
|
+
FileUtils.mv(
|
53
|
+
File.join(Config.tmp_path, "#{package.basename}-#{first_suffix}"),
|
54
|
+
File.join(Config.tmp_path, package.basename)
|
55
|
+
)
|
56
|
+
else
|
57
|
+
package.chunk_suffixes = suffixes
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Returns an array of suffixes for each chunk, in alphabetical order.
|
63
|
+
# For example: [aa, ab, ac, ad, ae] or [aaa, aab, aac aad]
|
64
|
+
def chunk_suffixes
|
65
|
+
chunks.map { |chunk| File.extname(chunk).split("-").last }.sort
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Returns an array of full paths to the backup chunks.
|
70
|
+
# Chunks are sorted in alphabetical order.
|
71
|
+
def chunks
|
72
|
+
Dir[File.join(Config.tmp_path, package.basename + "-*")].sort
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module Storage
|
5
|
+
class Base
|
6
|
+
include Config::Helpers
|
7
|
+
|
8
|
+
##
|
9
|
+
# Base path on the remote where backup package files will be stored.
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
##
|
13
|
+
# Number of backups to keep or time until which to keep.
|
14
|
+
#
|
15
|
+
# If an Integer is given it sets the limit to how many backups to keep in
|
16
|
+
# the remote location. If exceeded, the oldest will be removed to make
|
17
|
+
# room for the newest.
|
18
|
+
#
|
19
|
+
# If a Time object is given it will remove backups _older_ than the given
|
20
|
+
# date.
|
21
|
+
#
|
22
|
+
# @!attribute [rw] keep
|
23
|
+
# @param [Integer|Time]
|
24
|
+
# @return [Integer|Time]
|
25
|
+
attr_accessor :keep
|
26
|
+
|
27
|
+
attr_reader :model, :package, :storage_id
|
28
|
+
|
29
|
+
##
|
30
|
+
# +storage_id+ is a user-defined string used to uniquely identify
|
31
|
+
# multiple storages of the same type. If multiple storages of the same
|
32
|
+
# type are added to a single backup model, this identifier must be set.
|
33
|
+
# This will be appended to the YAML storage file used for cycling backups.
|
34
|
+
def initialize(model, storage_id = nil, &block)
|
35
|
+
@model = model
|
36
|
+
@package = model.package
|
37
|
+
@storage_id = storage_id.to_s.gsub(%r{\W}, "_") if storage_id
|
38
|
+
|
39
|
+
load_defaults!
|
40
|
+
instance_eval(&block) if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
def path=(value)
|
44
|
+
@path = value.dup
|
45
|
+
end
|
46
|
+
|
47
|
+
def perform!
|
48
|
+
Logger.info "#{storage_name} Started..."
|
49
|
+
transfer!
|
50
|
+
if respond_to?(:cycle!, true) && (keep.to_i > 0 || keep.is_a?(Time))
|
51
|
+
cycle!
|
52
|
+
end
|
53
|
+
Logger.info "#{storage_name} Finished!"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
##
|
59
|
+
# Return the remote path for the current or given package.
|
60
|
+
def remote_path(pkg = package)
|
61
|
+
path.empty? ? File.join(pkg.trigger, pkg.time) :
|
62
|
+
File.join(path, pkg.trigger, pkg.time)
|
63
|
+
end
|
64
|
+
alias remote_path_for remote_path
|
65
|
+
|
66
|
+
def storage_name
|
67
|
+
@storage_name ||= self.class.to_s.sub("Backup::", "") +
|
68
|
+
(storage_id ? " (#{storage_id})" : "")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|