backups-cli 1.0.9
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/.gitignore +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE +7 -0
- data/README.md +256 -0
- data/Rakefile +6 -0
- data/backups-cli.gemspec +25 -0
- data/bin/backups +47 -0
- data/lib/backups.rb +19 -0
- data/lib/backups/adapter/mysql.rb +202 -0
- data/lib/backups/base.rb +70 -0
- data/lib/backups/cli.rb +51 -0
- data/lib/backups/crontab.rb +200 -0
- data/lib/backups/driver/mysql.rb +35 -0
- data/lib/backups/events.rb +40 -0
- data/lib/backups/ext/fixnum.rb +21 -0
- data/lib/backups/ext/hash.rb +8 -0
- data/lib/backups/ext/nil_class.rb +7 -0
- data/lib/backups/ext/ordered_hash.rb +11 -0
- data/lib/backups/ext/string.rb +20 -0
- data/lib/backups/listener.rb +19 -0
- data/lib/backups/listeners/notify/datadog.rb +44 -0
- data/lib/backups/listeners/notify/slack.rb +121 -0
- data/lib/backups/loader.rb +55 -0
- data/lib/backups/logger.rb +8 -0
- data/lib/backups/runner.rb +137 -0
- data/lib/backups/stats/mysql.rb +80 -0
- data/lib/backups/system.rb +54 -0
- data/lib/backups/verify/mysql.rb +180 -0
- data/lib/backups/version.rb +4 -0
- metadata +173 -0
data/lib/backups/base.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Backups
|
2
|
+
class Base
|
3
|
+
|
4
|
+
ALL_DATABASES = "all-databases"
|
5
|
+
|
6
|
+
include System
|
7
|
+
include Loader
|
8
|
+
|
9
|
+
def initialize config
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_timestamp
|
14
|
+
now = Time.new
|
15
|
+
now = now.utc if now.utc?
|
16
|
+
now.iso8601.gsub(':', '-')
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_date_path
|
20
|
+
Time.now.strftime('%Y/%m/%d')
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_prefix
|
24
|
+
return @options['s3']['prefix'] if @options['s3']['prefix']
|
25
|
+
return @config["_name"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def compress source, dest, secret
|
29
|
+
if source.kind_of? Array
|
30
|
+
dir = File.dirname(source[0])
|
31
|
+
base = source.map {|v| File.basename(v)}.join(' ')
|
32
|
+
else
|
33
|
+
dir = File.dirname(source)
|
34
|
+
base = File.basename(source)
|
35
|
+
end
|
36
|
+
|
37
|
+
commands = []
|
38
|
+
commands << "cd #{dir} && zip"
|
39
|
+
commands << "--password #{secret}" if secret
|
40
|
+
commands << "-r #{dest} #{base}"
|
41
|
+
|
42
|
+
exec commands.join(' ')
|
43
|
+
end
|
44
|
+
|
45
|
+
def send_to_s3 bucket, path, filename, options = nil
|
46
|
+
dest = "s3://#{bucket}/#{path}"
|
47
|
+
$LOGGER.info "Sending to #{dest}"
|
48
|
+
exec "aws s3 cp #{filename} #{dest}/"
|
49
|
+
|
50
|
+
return unless options
|
51
|
+
tags = options.fetch('tags', {})
|
52
|
+
|
53
|
+
return unless tags.size > 0
|
54
|
+
key = "#{path}/#{File.basename(filename)}"
|
55
|
+
tag_s3_object bucket, key, tags
|
56
|
+
end
|
57
|
+
|
58
|
+
def tag_s3_object bucket, key, tags
|
59
|
+
tagSet = tags.map{ |k, v| "{Key=#{k},Value=#{v}}" }.join(",")
|
60
|
+
$LOGGER.info "Tagging s3://#{bucket}/#{key}"
|
61
|
+
$LOGGER.debug "Tagset: #{tagSet}"
|
62
|
+
|
63
|
+
exec "aws s3api put-object-tagging \
|
64
|
+
--bucket #{bucket} \
|
65
|
+
--key #{key} \
|
66
|
+
--tagging 'TagSet=[#{tagSet}]'"
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
data/lib/backups/cli.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module Backups
|
4
|
+
class Cli < Thor
|
5
|
+
|
6
|
+
desc "version", "Show current version"
|
7
|
+
def version
|
8
|
+
puts "v#{VERSION}"
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "ls", "Lists all the configured jobs"
|
12
|
+
def ls
|
13
|
+
Crontab.new.list()
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "show [JOB]", "Shows the merged config (for a JOB or them all)"
|
17
|
+
def show job = nil
|
18
|
+
data = Runner.new.show(job)
|
19
|
+
puts data.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "start [JOB]", "Starts a backup JOB or all of them"
|
23
|
+
def start job = nil
|
24
|
+
if job
|
25
|
+
Runner.new.start job
|
26
|
+
else
|
27
|
+
Runner.new.start_all
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "verify [JOB]", "Restores and verifies a backup JOB or all of them"
|
32
|
+
def verify job = nil
|
33
|
+
if job
|
34
|
+
Runner.new.verify job
|
35
|
+
else
|
36
|
+
Runner.new.verify_all
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "install", "Sets up the crontab for all jobs"
|
41
|
+
def install
|
42
|
+
Crontab.new.install
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "crontab", "Shows the crontab config"
|
46
|
+
def crontab
|
47
|
+
puts Crontab.new.show()
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require "tablelize"
|
2
|
+
|
3
|
+
module Backups
|
4
|
+
class Crontab
|
5
|
+
|
6
|
+
include System
|
7
|
+
include Loader
|
8
|
+
|
9
|
+
PADDING = 10
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
load_configs
|
13
|
+
@base = File.expand_path('../../..', __FILE__)
|
14
|
+
end
|
15
|
+
|
16
|
+
def list
|
17
|
+
paddings = {
|
18
|
+
'job' => 20,
|
19
|
+
'backup' => 10,
|
20
|
+
'verify' => 10,
|
21
|
+
}
|
22
|
+
|
23
|
+
rows = []
|
24
|
+
rows << ["JOB", "CRONTAB", "INSTALL", "ENABLED"]
|
25
|
+
|
26
|
+
$GLOBAL["jobs"].each do |job, config|
|
27
|
+
backup = config.fetch('backup', {})
|
28
|
+
crontab = backup.fetch('crontab', {})
|
29
|
+
crontime = get_crontime(crontab)
|
30
|
+
install = crontab.fetch('install', true)
|
31
|
+
enabled = config.fetch('enabled', true)
|
32
|
+
|
33
|
+
rows << [job, crontime, install, enabled]
|
34
|
+
end
|
35
|
+
|
36
|
+
Tablelize::table rows
|
37
|
+
end
|
38
|
+
|
39
|
+
def show
|
40
|
+
get_install_lines()
|
41
|
+
end
|
42
|
+
|
43
|
+
def install
|
44
|
+
last_log_active = $LOG_ACTIVE
|
45
|
+
$LOG_ACTIVE = 0
|
46
|
+
|
47
|
+
install_mysql_groups
|
48
|
+
|
49
|
+
content = get_install_lines()
|
50
|
+
cronfile = "/tmp/crontab.new"
|
51
|
+
write cronfile, content
|
52
|
+
exec "crontab #{cronfile}"
|
53
|
+
exec "rm #{cronfile}"
|
54
|
+
exec "crontab -l"
|
55
|
+
|
56
|
+
$LOG_ACTIVE = last_log_active
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def get_install_lines
|
61
|
+
# We dont want passwords in the main log and write does that
|
62
|
+
|
63
|
+
start = "# BEGIN SECTION Backups\n"
|
64
|
+
finish = "# END SECTION Backups\n"
|
65
|
+
previous = `crontab -l 2>/dev/null`
|
66
|
+
|
67
|
+
backups = $GLOBAL.fetch("backups", {})
|
68
|
+
crontab = backups.fetch("crontab", {})
|
69
|
+
header = crontab.fetch("header", "")
|
70
|
+
backup = crontab.fetch("backup", {})
|
71
|
+
verify = crontab.fetch("verify", {})
|
72
|
+
|
73
|
+
backup_all = backup.fetch("run-all", false)
|
74
|
+
verify_all = verify.fetch("run-all", false)
|
75
|
+
|
76
|
+
content = "#{header}\n#{previous}" \
|
77
|
+
if header and not previous.include? header
|
78
|
+
|
79
|
+
content << start
|
80
|
+
content << "# Generated at #{Time.now}\n"
|
81
|
+
|
82
|
+
if backup_all
|
83
|
+
content << get_all_backup(backup) + "\n"
|
84
|
+
else
|
85
|
+
content << get_jobs_crontab() + "\n"
|
86
|
+
end
|
87
|
+
|
88
|
+
if verify_all
|
89
|
+
content << get_all_verify(verify) + "\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
content << finish
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_all_backup crontab
|
96
|
+
line = "#{@base}/bin/backups start"
|
97
|
+
minute = crontab.fetch("minute", 0)
|
98
|
+
hour = crontab.fetch("hour", "*")
|
99
|
+
crontime = get_crontime(crontab)
|
100
|
+
prefix = crontab.fetch("prefix", "")
|
101
|
+
postfix = crontab.fetch("postfix", "")
|
102
|
+
|
103
|
+
line = "#{prefix} #{line}" if prefix
|
104
|
+
line = "#{line} #{postfix}" if postfix
|
105
|
+
|
106
|
+
"#{crontime} #{line}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def get_all_verify crontab
|
110
|
+
line = "#{@base}/bin/backups verify"
|
111
|
+
minute = crontab.fetch("minute", 0)
|
112
|
+
hour = crontab.fetch("hour", "*")
|
113
|
+
crontime = get_crontime(crontab)
|
114
|
+
prefix = crontab.fetch("prefix", "")
|
115
|
+
postfix = crontab.fetch("postfix", "")
|
116
|
+
|
117
|
+
line = "#{prefix} #{line}" if prefix
|
118
|
+
line = "#{line} #{postfix}" if postfix
|
119
|
+
|
120
|
+
"#{crontime} #{line}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_jobs_crontab
|
124
|
+
contents = []
|
125
|
+
|
126
|
+
$GLOBAL['jobs'].each do |job, config|
|
127
|
+
backup = config.fetch('backup', {})
|
128
|
+
crontab = backup.fetch('crontab', {})
|
129
|
+
crontime = get_cronjob(job, 'start', crontab)
|
130
|
+
contents << crontime
|
131
|
+
end
|
132
|
+
|
133
|
+
contents.join("\n")
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_cronjob job, command, crontab
|
137
|
+
return unless crontab.fetch('install', true)
|
138
|
+
line = "#{@base}/bin/backups #{command} #{job}"
|
139
|
+
time = get_crontime(crontab)
|
140
|
+
|
141
|
+
prefix = crontab.fetch("prefix", "")
|
142
|
+
postfix = crontab.fetch("postfix", "")
|
143
|
+
line = "#{prefix} #{line}" if prefix
|
144
|
+
line = "#{line} #{postfix}" if postfix
|
145
|
+
|
146
|
+
"#{time} #{line}" if not time.nil?
|
147
|
+
end
|
148
|
+
|
149
|
+
def get_crontime crontab
|
150
|
+
time = "#{crontab.fetch('minute', '0')}"
|
151
|
+
time << " #{crontab.fetch('hour', '4')}"
|
152
|
+
time << " #{crontab.fetch('day', '*')}"
|
153
|
+
time << " #{crontab.fetch('month', '*')}"
|
154
|
+
time << " #{crontab.fetch('weekday', '*')}"
|
155
|
+
|
156
|
+
time.strip()
|
157
|
+
end
|
158
|
+
|
159
|
+
def install_mysql_groups
|
160
|
+
$GLOBAL["jobs"].each do |job, config|
|
161
|
+
install_mysql_group job, config if config["type"].downcase === "mysql"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def install_mysql_group job, config
|
166
|
+
group = "client_" + job.gsub("-", "_")
|
167
|
+
username = config["backup"]["connection"]["username"]
|
168
|
+
password = config["backup"]["connection"]["password"]
|
169
|
+
|
170
|
+
contents = []
|
171
|
+
contents << "[" + group + "]"
|
172
|
+
contents << "user = " + username if username
|
173
|
+
contents << "password = " + password if password
|
174
|
+
replace_mysql_group group, contents.join("\n")
|
175
|
+
end
|
176
|
+
|
177
|
+
def replace_mysql_group group, content
|
178
|
+
path = File.expand_path("~/.my.cnf")
|
179
|
+
lines = []
|
180
|
+
if File.exist? path
|
181
|
+
found = false
|
182
|
+
File.readlines(path).each do |line|
|
183
|
+
if line === "[#{group}]\n"
|
184
|
+
found = true
|
185
|
+
elsif line.match(/^\[.*\]\n/)
|
186
|
+
found = false
|
187
|
+
end
|
188
|
+
lines << line if not found
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
contents = lines.join()
|
193
|
+
contents += "\n" if contents.size > 0 and contents[contents.size-1..-1] != "\n"
|
194
|
+
contents += content
|
195
|
+
|
196
|
+
write path, contents
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "mysql2"
|
2
|
+
|
3
|
+
module Backups
|
4
|
+
module Driver
|
5
|
+
module Mysql
|
6
|
+
|
7
|
+
def connect
|
8
|
+
@connection[flags: Mysql2::Client::MULTI_STATEMENTS]
|
9
|
+
@mysql = Mysql2::Client.new(@connection)
|
10
|
+
end
|
11
|
+
|
12
|
+
def exec_query(sql)
|
13
|
+
return puts sql if $dry_run
|
14
|
+
connect if not @mysql
|
15
|
+
@mysql.query sql
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_results(sql)
|
19
|
+
return [] if not rset = exec_query(sql)
|
20
|
+
rows = []
|
21
|
+
rset.each do |row|
|
22
|
+
rows << row
|
23
|
+
end
|
24
|
+
|
25
|
+
return rows
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_result(sql)
|
29
|
+
rows = get_results(sql)
|
30
|
+
return rows[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Backups
|
2
|
+
class Events
|
3
|
+
|
4
|
+
@@events = {}
|
5
|
+
|
6
|
+
def self.on *events, &block
|
7
|
+
names = []
|
8
|
+
if events.kind_of? Array
|
9
|
+
names = events
|
10
|
+
else
|
11
|
+
names << events
|
12
|
+
end
|
13
|
+
|
14
|
+
names.each do |name|
|
15
|
+
@@events[name] ||= []
|
16
|
+
@@events[name] << block
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.fire event, params
|
21
|
+
params[:event] = event
|
22
|
+
names = []
|
23
|
+
if event.kind_of? Array
|
24
|
+
names = event
|
25
|
+
else
|
26
|
+
names << event
|
27
|
+
end
|
28
|
+
|
29
|
+
names.each do |name|
|
30
|
+
if @@events.has_key? name
|
31
|
+
@@events[name].each do |cb|
|
32
|
+
res = cb.call(params)
|
33
|
+
break if res == false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
unless Fixnum.method_defined?(:european)
|
2
|
+
class ::Fixnum
|
3
|
+
def european
|
4
|
+
self.to_s.reverse.gsub(/...(?=.)/,'\& ').reverse
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
unless Fixnum.method_defined?(:to_filesize)
|
10
|
+
class ::Fixnum
|
11
|
+
def to_filesize
|
12
|
+
{
|
13
|
+
'B' => 1024,
|
14
|
+
'KB' => 1024 * 1024,
|
15
|
+
'MB' => 1024 * 1024 * 1024,
|
16
|
+
'GB' => 1024 * 1024 * 1024 * 1024,
|
17
|
+
'TB' => 1024 * 1024 * 1024 * 1024 * 1024
|
18
|
+
}.each_pair {|e, s| return "#{(self.to_f / (s/1024)).round(2)} #{e}" if self < s }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
unless BSON::OrderedHash.method_defined?(:to_json)
|
2
|
+
class BSON::OrderedHash
|
3
|
+
def to_h
|
4
|
+
inject({}) { |acc, element| k,v = element; acc[k] = (if v.class == BSON::OrderedHash then v.to_h else v end); acc }
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_json
|
8
|
+
to_h.to_json
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|