-
Notifications
You must be signed in to change notification settings - Fork 0
/
rbd-backup
executable file
·221 lines (186 loc) · 8.42 KB
/
rbd-backup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/ruby
require "json"
require "open3"
require __dir__ + "/lib/color.rb"
require __dir__ + "/lib/shout.rb"
require __dir__ + "/lib/parser.rb"
parser, ceph_prefix, ceph_args = doParse
parser.banner = "Usage: rbd-backup [options] pool [volume]"
parser.types!
parser.show_banner() && exit if parser.cli[:help]
parser.bound_cli(:pool, 0)
parser.bound_cli(:image, 1)
pid_file = parser.config[:general][:pid_dir] + "/rbd-backup.pid"
if File.exists? pid_file
pid = File.read(pid_file).strip
pid_exist = `ps -p #{pid} -o pid= | wc -l`.strip.to_i
shout("already running, exit", "ERR") if pid_exist == 1
exit if pid_exist == 1
end
File.write(pid_file, Process.pid)
begin
## get ceph pools
pools = `#{ceph_prefix}ceph #{ceph_args} osd lspools --format json 2>/dev/null`
pools = JSON.parse(pools)
found = false
pools.each {|pool| found = true if pool["poolname"] == parser.cli[:pool] }
shout( "No pool with name '#{parser.cli[:pool]}' ", "ERR" ) if not found
exit if not found
rescue Exception => e
shout( "Error while get ceph pools: #{e}", "ERR" )
exit
end
begin
## get ceph volumes in pool
rbd_images = `#{ceph_prefix}rbd #{ceph_args} --format json ls -l #{parser.cli[:pool]} 2>/dev/null`
rbd_images = JSON.parse(rbd_images)
rescue Exception => e
shout( "Error parsing 'ceph ls -l' output: #{e}", "ERR" )
exit
end
## init resulting hash
images = Hash[:rbd => Hash[], :total => Hash[:pool => parser.cli[:pool], :count => 0, :exported => 0, :archived => 0, :export_errors => 0, :archive_errors => 0] ]
images[:total][:export_start] = Time.now.to_i
images[:total][:archive_start] = Time.now.to_i
exclude_file = parser.config[:rbd_backup][:exclude_file]
exclude_file = __dir__ + "/etc/exclude" if exclude_file == "etc/exclude"
excludes = ""
excludes = File.read(exclude_file) if File.exists? exclude_file
rbd_images.each do |rbd_image|
## loop through volumes
image_uuid = rbd_image["image"]
next if image_uuid != parser.cli[:image] && parser.cli[:image] != nil
next if excludes =~ /#{image_uuid}/
if images[:rbd][image_uuid] == nil
images[:rbd][image_uuid] = Hash[:exported => false, :archived => false, :snaps => Array.new]
end
## current volume has no snapshots
next if rbd_image["snapshot"] == nil
if rbd_image["snapshot"] =~ /BKP_([0-9]+\.){2}[0-9]+/
## found old snapshot
images[:rbd][image_uuid][:snaps].push(rbd_image["snapshot"])
end
end
excludes = nil
images[:total][:count] = images[:rbd].size
if images[:total][:count] == 0
## check pool/volume existense
if parser.cli[:image] != nil
shout( "No volume with name #{parser.cli[:image]} found", "ERR" )
else
shout( "No images in pool #{parser.cli[:pool]} found", "ERR" )
end
exit
end
def doExport(pool, image, export_dir, ceph_prefix, ceph_args, export_fname)
snap_date = Time.now().strftime("%Y.%m.%d")
fname = export_fname.gsub("$POOL", pool).gsub("$VOLUME", image)
fname = Time.now().strftime(fname)
snap_name = "BKP_#{snap_date}"
snap_path = "#{pool}/#{image}@#{snap_name}"
begin
## actual export code
`#{ceph_prefix}rbd #{ceph_args} snap create #{snap_path}`
`#{ceph_prefix}rbd #{ceph_args} export #{snap_path} #{export_dir}/#{fname}`
rescue Exception => e
return fname, snap_name, e
end
return fname, snap_name, nil
end
def doArchive(snap_file, arch_cmd, export_dir)
## actual archiving code
Open3.popen3("#{arch_cmd} #{export_dir}/#{snap_file}") {|i,o,e,t|
if not t.value.success?
err = o.read if e.read.empty?
err = e.read if not e.read.empty?
return err
else
return nil
end
}
end
## init threads structures
threads = []
queue = Hash[:export => Array.new, :archive => Array.new]
THREADS = Hash[:act_export => 0, :act_archive => 0]
THREADS[:max_export] = parser.config[:rbd_backup][:max_export_threads]
THREADS[:max_archive] = parser.config[:rbd_backup][:max_archive_threads]
images[:rbd].each do |image_uuid, image|
## first push each volume to export queue
queue[:export].push(image_uuid)
## main export threading facility. FIXME: refactor this
threads << Thread.new do
## wait if we reached MAX_EXPORT threads count
sleep 1 while THREADS[:act_export] >= THREADS[:max_export]
THREADS[:act_export] += 1
## start export
img = queue[:export].shift
shout( "[#{THREADS[:act_export]}/#{images[:total][:exported]}/#{images[:total][:count]}] #{img}", "INFO", "EXPORT" )
image[:export_start] = Time.now.to_i
fname, snap_name, error = doExport(parser.cli[:pool], img, parser.config[:rbd_backup][:export_dir], ceph_prefix, ceph_args, parser.config[:rbd_backup][:export_fname])
image[:export_end] = Time.now.to_i
image[:exported] = true if error == nil
images[:total][:export_errors] += 1 if error != nil
shout( "Export function error: #{error}", "ERR" ) if error != nil
## remove snapshot in place if SNAP_KEEP is set to 0
if parser.config[:rbd_backup][:snap_keep] == 0
shout( "removing snapshot #{parser.cli[:pool]}/#{img}@#{snap_name}" )
`#{ceph_prefix}rbd #{ceph_args} snap rm #{parser.cli[:pool]}/#{img}@#{snap_name}`
else
image[:snaps].push(snap_name)
end
images[:total][:export_end] = Time.now.to_i
THREADS[:act_export] -= 1
images[:total][:exported] += 1
## add volume to archive queue
queue[:archive].push( fname ) if parser.config[:rbd_backup][:arch_cmd] != ""
end
## main archive threading facility. FIXME: refactor this
threads << Thread.new do
## wait if we reached MAX_ARCHIVE threads count or queue is empty
sleep 1 while THREADS[:act_archive] >= THREADS[:max_archive] || queue[:archive].empty?
THREADS[:act_archive] += 1
## start archiving
fname = queue[:archive].shift
shout( "[#{THREADS[:act_archive]}/#{images[:total][:archived]}/#{images[:total][:count]}] #{fname}", "INFO", "ARCHIVE" )
image[:archive_start] = Time.now.to_i
error = doArchive(fname, parser.config[:rbd_backup][:arch_cmd], parser.config[:rbd_backup][:export_dir])
image[:archive_end] = Time.now.to_i
image[:archived] = true if error == nil
images[:total][:archive_errors] += 1 if error != nil
shout( "Archive function error: #{error}", "ERR" ) if error != nil
## remove exported file in place if LOCAL_KEEP is set to 0
if parser.config[:rbd_backup][:local_keep] == 0
shout( "removing local file #{parser.config[:rbd_backup][:export_dir]}/#{fname}" )
begin
File.delete("#{parser.config[:rbd_backup][:export_dir]}/#{fname}")
rescue Exception => e
shout("error removing file #{parser.config[:rbd_backup][:export_dir]}/#{fname}: #{e}", "ERR")
end
end
THREADS[:act_archive] -= 1
images[:total][:archived] += 1
end
end
threads.each {|thr| thr.join}
images[:total][:archive_end] = Time.now.to_i
shout("Starting rotate process")
images[:rbd].each do |image, stat|
snaps = stat[:snaps]
snaps.sort!.reverse!
## rotate snapshots in cluster
while snaps.count > parser.config[:rbd_backup][:snap_keep] && snaps.count != 0
shout( "removing snapshot: #{image}@#{snaps[snaps.count-1]}" )
`#{ceph_prefix}rbd #{ceph_args} snap rm #{parser.cli[:pool]}/#{image}@#{snaps[snaps.count-1]}`
snaps.delete_at(snaps.count-1)
end
## rotate local files
local_files = Dir["#{parser.config[:rbd_backup][:export_dir]}/*#{image}*"].sort!.reverse!
while local_files.count > parser.config[:rbd_backup][:local_keep] && local_files.count != 0
shout( "removing local file: #{local_files[local_files.count-1]}" )
File.delete(local_files[local_files.count-1])
local_files.delete_at(local_files.count-1)
end
end
## write statistics
File.write(parser.config[:rbd_backup][:stat_file], images.to_json)