backup_utility 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/backup_utility.rb +3 -0
- data/lib/backup_utility/base.rb +228 -0
- data/lib/backup_utility/file.rb +98 -0
- data/lib/backup_utility/mongo.rb +37 -0
- data/lib/backup_utility/postgres.rb +260 -0
- metadata +68 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
CODE_DIR = '/home/twist/code'
|
4
|
+
BACKUP_DIR = '/home/twist/backups'
|
5
|
+
SHARED_DIR = '/mnt/shared_storage2/database/backups'
|
6
|
+
class BackupUtility::Base
|
7
|
+
def initialize(backup_info)
|
8
|
+
@backup_dir = backup_info[:backup_dir] || BACKUP_DIR
|
9
|
+
@shared_dir = backup_info[:shared_dir] || SHARED_DIR
|
10
|
+
@code_dir = backup_info[:code_dir] || CODE_DIR
|
11
|
+
@backup_machine = backup_info[:backup_machine] || 'a1-stats'
|
12
|
+
|
13
|
+
@send_to_shared = backup_info.fetch(:send_to_shared, true)
|
14
|
+
@send_to_s3 = backup_info.fetch(:send_to_s3, true)
|
15
|
+
@move_backups = backup_info.fetch(:move_backups, true)
|
16
|
+
@use_code = backup_info.fetch(:use_code, false)
|
17
|
+
@use_scp = backup_info.fetch(:use_scp, false)
|
18
|
+
@save = backup_info.fetch(:save, true)
|
19
|
+
@clean_shared_age = backup_info[:clean_shared_age]
|
20
|
+
@clean_shared_age = ::Rails.env == 'production' ? 30 : 14 if @clean_shared_age.blank?
|
21
|
+
|
22
|
+
@clean_backups_age = backup_info[:clean_backups_age]
|
23
|
+
if @clean_backups_age.blank?
|
24
|
+
if ::Rails.env == 'production'
|
25
|
+
@cleanup_backups_age = @move_backups ? 7 : 14
|
26
|
+
else
|
27
|
+
@cleanup_backups_age = @move_backups ? 3 : 7
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@last_dst = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def log_and_time(out)
|
34
|
+
log(out)
|
35
|
+
start = Time.now
|
36
|
+
ret = yield
|
37
|
+
took = Time.now - start
|
38
|
+
log("took #{took}")
|
39
|
+
ret
|
40
|
+
end
|
41
|
+
|
42
|
+
def log(out)
|
43
|
+
puts "#{Time.now} - #{out}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def backup(settings = {}, append = nil)
|
47
|
+
now = Time.now
|
48
|
+
storage_name, working_dir, backup_file = get_paths(settings, append)
|
49
|
+
if !perform_dump(settings, working_dir)
|
50
|
+
log("could not perform dump properly")
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
if custom?
|
54
|
+
# no need to tar up the directory, all we need to do is rename
|
55
|
+
# the file in the backup_dir to the backup name
|
56
|
+
log("moving file #{@last_dst} to #{backup_file}")
|
57
|
+
File.mv(@last_dst, backup_file)
|
58
|
+
ret = 0
|
59
|
+
else
|
60
|
+
base_name = File.basename(working_dir)
|
61
|
+
out = log_and_time("taring up backup file #{backup_file}") do
|
62
|
+
# tar up the combined directory
|
63
|
+
`tar czf #{backup_file} -C #{@backup_dir} #{base_name}`
|
64
|
+
end
|
65
|
+
ret=$?
|
66
|
+
end
|
67
|
+
if ret.to_i != 0
|
68
|
+
log("error taring file : #{out}")
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
if File.exists?(backup_file)
|
72
|
+
log("cleaning up working dir #{@working_dir}")
|
73
|
+
FileUtils.rm_rf(working_dir)
|
74
|
+
else
|
75
|
+
log("backup file #{backup_file} was not created properly")
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
store_backup(storage_name, backup_file)
|
79
|
+
taken = Time.now - now
|
80
|
+
log("time taken to dump backup #{taken}")
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_paths(settings = {}, append = nil)
|
84
|
+
append = settings[:append] if append.nil?
|
85
|
+
prepend = settings[:prepend]
|
86
|
+
now = Time.now
|
87
|
+
storage_name = "#{::Rails.env}_latest"
|
88
|
+
format = settings.fetch(:date_format, '%Y-%m-%d-%H')
|
89
|
+
date = now.strftime(format)
|
90
|
+
storage_name += "_#{append}" if append
|
91
|
+
base_name = "#{prepend}#{date}-#{::Rails.env}"
|
92
|
+
base_name += "-#{append}" if append
|
93
|
+
if custom?
|
94
|
+
ext = '.dump.gz'
|
95
|
+
else
|
96
|
+
ext = '.tar.gz'
|
97
|
+
end
|
98
|
+
storage_name += ext
|
99
|
+
back_name = "#{base_name}#{ext}"
|
100
|
+
working_dir = File.join(@backup_dir, base_name)
|
101
|
+
Dir.mkdir(working_dir) if !File.exists?(working_dir)
|
102
|
+
backup_file = File.join(@backup_dir, back_name)
|
103
|
+
[storage_name, working_dir, backup_file]
|
104
|
+
end
|
105
|
+
|
106
|
+
def custom?
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
def store_backup(storage_name, backup_file)
|
111
|
+
if @save
|
112
|
+
# send file to s3
|
113
|
+
if @send_to_s3
|
114
|
+
log("sending backup file to s3")
|
115
|
+
send_file_to_s3(backup_file)
|
116
|
+
end
|
117
|
+
if @send_to_shared
|
118
|
+
if @use_scp
|
119
|
+
# maintain the name so we can send it to the storage properly
|
120
|
+
back_name = File.basename(backup_file)
|
121
|
+
shared_file = File.join(@shared_dir, back_name)
|
122
|
+
log("scping backup file to #{shared_file}")
|
123
|
+
`/usr/bin/scp #{backup_file} #{@backup_user}@#{@backup_machine}:#{shared_file}`
|
124
|
+
else
|
125
|
+
shared_file = File.join(@shared_dir, storage_name)
|
126
|
+
log("copying backup file to #{shared_file}")
|
127
|
+
FileUtils.copy_file(backup_file, shared_file)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
cleanup_backups
|
131
|
+
cleanup_shared
|
132
|
+
else
|
133
|
+
log("skip save")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def shared_dst(create_if_missing = false)
|
138
|
+
return nil if !File.exists?(@shared_dir)
|
139
|
+
dst = File.join(@shared_dir, "#{::Rails.env}_daily")
|
140
|
+
Dir.mkdir(dst) if !File.exists?(dst) && create_if_missing
|
141
|
+
dst
|
142
|
+
end
|
143
|
+
|
144
|
+
def cleanup_backups(age = nil)
|
145
|
+
return false if !File.exists?(@backup_dir)
|
146
|
+
age = @cleanup_backups_age if age.blank?
|
147
|
+
|
148
|
+
clean_dir = @backup_dir
|
149
|
+
cleanup_dir(clean_dir, age) do |file|
|
150
|
+
if @move_backups
|
151
|
+
dst_file = shared_dst(true)
|
152
|
+
next if dst_file.blank? || !File.exists?(dst_file)
|
153
|
+
FileUtils.mv(file, dst_file)
|
154
|
+
else
|
155
|
+
File.delete(file)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def cleanup_shared(age = nil)
|
161
|
+
clean_dir = shared_dst
|
162
|
+
return false if clean_dir.nil? || !File.exists?(clean_dir)
|
163
|
+
age = @clean_shared_age if age.blank?
|
164
|
+
cleanup_dir(clean_dir, age) do |file|
|
165
|
+
File.delete(file)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def cleanup_dir(clean_dir, age, display = true)
|
170
|
+
cleaned = 0
|
171
|
+
total = 0
|
172
|
+
Dir["#{clean_dir}/*.tar.gz"].each do |file|
|
173
|
+
stat = File.stat(file)
|
174
|
+
if stat.mtime < age.to_i.days.ago
|
175
|
+
yield file
|
176
|
+
cleaned += stat.size
|
177
|
+
total += 1
|
178
|
+
end
|
179
|
+
end
|
180
|
+
log("cleaned up #{total} for #{cleaned} bytes in #{clean_dir}") if display
|
181
|
+
[total, cleaned]
|
182
|
+
end
|
183
|
+
|
184
|
+
def send_dir_to_s3
|
185
|
+
Dir.foreach(@backup_dir) do |f|
|
186
|
+
next if f[0] == ?.
|
187
|
+
file = File.join(backup_dir, f)
|
188
|
+
send_file_to_s3(file)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def send_file_to_s3(backup_file)
|
193
|
+
if @use_code
|
194
|
+
`/usr/bin/python #{@code_dir}/S3Storage.py upload #{backup_file}`
|
195
|
+
return true
|
196
|
+
end
|
197
|
+
bucket = 'twistage-backup'
|
198
|
+
# first check if backup file exists on s3, and then upload it
|
199
|
+
name = File.basename(backup_file)
|
200
|
+
s3 = S3Storage.default_connection
|
201
|
+
files = s3.list_bucket({:prefix => name}, bucket)
|
202
|
+
size = File.size(backup_file)
|
203
|
+
amt = 5
|
204
|
+
if files.size == 0
|
205
|
+
amt.times do |x|
|
206
|
+
log("uploading #{backup_file} to s3")
|
207
|
+
begin
|
208
|
+
start = Time.now
|
209
|
+
s3.put_file(backup_file, bucket)
|
210
|
+
taken = Time.now - start
|
211
|
+
mb = (size / 1.megabyte) / taken
|
212
|
+
log("file uploaded in #{taken} seconds (#{mb} MB/s)")
|
213
|
+
return true
|
214
|
+
rescue StandardError => err
|
215
|
+
if x+1 == amt
|
216
|
+
log("could not upload file #{backup_file} : #{err.to_s}")
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
log("could not send file #{err.to_s}, retrying (#{x+1} of #{amt})")
|
220
|
+
sleep(15)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
else
|
224
|
+
log("#{backup_file} already exists in s3")
|
225
|
+
end
|
226
|
+
true
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
class BackupUtility::File < BackupUtility::Base
|
2
|
+
|
3
|
+
def self.init(type)
|
4
|
+
save = ENV['save'] != 'false'
|
5
|
+
use_code = false
|
6
|
+
# don't send to shared, since we're going do it on "cleanup"
|
7
|
+
if ::Rails.env == 'production'
|
8
|
+
send_to_s3 = true
|
9
|
+
else
|
10
|
+
send_to_s3 = false
|
11
|
+
end
|
12
|
+
use_code = send_to_s3
|
13
|
+
settings = build_settings(type)
|
14
|
+
return [nil, nil] if settings.nil?
|
15
|
+
db_info = {:save => save,
|
16
|
+
:send_to_s3 => send_to_s3,
|
17
|
+
:send_to_shared => false,
|
18
|
+
:use_code => use_code,
|
19
|
+
:shared_dir => "/mnt/shared_storage2/backups/#{type}",
|
20
|
+
:backup_dir => "/home/twist/backups/#{type}",
|
21
|
+
:clean_shared_age => ENV['clean_shared_age']
|
22
|
+
}
|
23
|
+
[BackupUtility::File.new(db_info), settings]
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build_settings(type)
|
27
|
+
age = (ENV['age'] || 5).to_i
|
28
|
+
if type == :custom
|
29
|
+
src_dir = ENV['custom_dir']
|
30
|
+
label = ENV['custom_label']
|
31
|
+
if src_dir.nil? || label.nil?
|
32
|
+
puts "missing custom dir or label"
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
type = label.to_sym
|
36
|
+
else
|
37
|
+
type = type.to_sym
|
38
|
+
map = {:logs => '/home/twist/stats_prod/shared/nginx_logs/archive'
|
39
|
+
}
|
40
|
+
if map.has_key?(type)
|
41
|
+
src_dir = map[type]
|
42
|
+
else
|
43
|
+
# check if the type exists on disk
|
44
|
+
src_dir = "/home/twist/stats_prod/shared/log/#{type}_dumps"
|
45
|
+
end
|
46
|
+
if src_dir.blank? || !File.exists?(src_dir)
|
47
|
+
puts "could not find directory to backup for #{type} (src dir : #{src_dir})"
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
{:type => type, :src_dir => src_dir, :age => age.days.ago, :date_format => '%Y-%m-%d'}
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(backup_info)
|
55
|
+
super(backup_info)
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_paths(settings = {}, append = nil)
|
59
|
+
to_backup = find_dir(settings)
|
60
|
+
return [nil, nil, nil] if to_backup.nil?
|
61
|
+
date = File.basename(to_backup)
|
62
|
+
type = settings[:type]
|
63
|
+
base_name = "stats_#{type}_#{date}.tar.gz"
|
64
|
+
backup_file = File.join(@backup_dir, base_name)
|
65
|
+
working_dir = File.join(@backup_dir, date)
|
66
|
+
settings[:to_backup] = to_backup
|
67
|
+
[nil, working_dir, backup_file]
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_dir(settings)
|
71
|
+
src_dir = settings[:src_dir]
|
72
|
+
return nil if src_dir.blank? || !File.exists?(src_dir)
|
73
|
+
age = settings[:age]
|
74
|
+
to_backup = nil
|
75
|
+
Dir.entries(src_dir).each do |name|
|
76
|
+
next if name[0] == ?.
|
77
|
+
dir = File.join(src_dir, name)
|
78
|
+
next if !File.directory?(dir)
|
79
|
+
mtime = File.mtime(dir)
|
80
|
+
if mtime < age
|
81
|
+
to_backup = dir
|
82
|
+
puts "found #{dir} to backup"
|
83
|
+
break
|
84
|
+
end
|
85
|
+
end
|
86
|
+
to_backup
|
87
|
+
end
|
88
|
+
|
89
|
+
def perform_dump(settings, working_dir)
|
90
|
+
return false if working_dir.blank?
|
91
|
+
# move everything in the to_backup dir to the working_dir, and then remove it
|
92
|
+
to_backup = settings[:to_backup]
|
93
|
+
return false if to_backup.blank?
|
94
|
+
`mv #{to_backup} #{@backup_dir}`
|
95
|
+
return false if File.exists?(to_backup)
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class BackupUtility::Mongo < BackupUtility::Base
|
2
|
+
|
3
|
+
def self.init(include_env = true)
|
4
|
+
save = ENV['save'] != 'false'
|
5
|
+
if ::Rails.env == 'staging'
|
6
|
+
send_to_s3 = false
|
7
|
+
send_to_shared = true
|
8
|
+
elsif ::Rails.env == 'production'
|
9
|
+
send_to_s3 = true
|
10
|
+
send_to_shared = true
|
11
|
+
elsif ::Rails.env == 'development'
|
12
|
+
send_to_s3 = false
|
13
|
+
send_to_shared = false
|
14
|
+
end
|
15
|
+
use_code = send_to_s3
|
16
|
+
db_info = {:backup_dir => ENV['backup_dir'],
|
17
|
+
:save => save,
|
18
|
+
:send_to_s3 => send_to_s3,
|
19
|
+
:send_to_shared => send_to_shared,
|
20
|
+
:use_code => use_code
|
21
|
+
}
|
22
|
+
BackupUtility::Mongo.new(db_info)
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform_dump(settings, working_dir)
|
26
|
+
type = settings[:server_type] || 'data_store'
|
27
|
+
server = MongoServer.select(type)
|
28
|
+
return false if server.blank?
|
29
|
+
db_names = settings[:db_names] || 'videos_day,tracks_day,ref'
|
30
|
+
db_names = db_names.split(',')
|
31
|
+
db_names.each do |db_name|
|
32
|
+
puts "dumping mongo db #{db_name} to #{working_dir}"
|
33
|
+
server.dump(working_dir, db_name)
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
BIN='/usr/bin'
|
4
|
+
|
5
|
+
class BackupUtility::Postgres < BackupUtility::Base
|
6
|
+
|
7
|
+
def self.init(append = 'twist', include_env = true)
|
8
|
+
save = ENV['save'] != 'false'
|
9
|
+
move_backups = true
|
10
|
+
use_scp = use_code = false
|
11
|
+
if ::Rails.env == 'staging'
|
12
|
+
pg_user = 'admin'
|
13
|
+
db_name = "#{append}_stage"
|
14
|
+
send_to_s3 = false
|
15
|
+
send_to_shared = true
|
16
|
+
elsif ::Rails.env == 'production'
|
17
|
+
pg_user = "#{append}_db"
|
18
|
+
db_name = "#{append}_prod"
|
19
|
+
send_to_s3 = true
|
20
|
+
send_to_shared = true
|
21
|
+
use_scp = use_code = append == 'stats'
|
22
|
+
# move the backup files to shared, but only if its not stats, since it does its own thing
|
23
|
+
move_backups = append != 'stats'
|
24
|
+
elsif ::Rails.env == 'development'
|
25
|
+
pg_user = ENV['pg_user'] || 'admin'
|
26
|
+
db_name = ENV['db_name'] || "#{append}_dev"
|
27
|
+
send_to_s3 = false
|
28
|
+
# always set to false
|
29
|
+
save = false
|
30
|
+
send_to_shared = false
|
31
|
+
end
|
32
|
+
db_info = {:db_name => db_name, :pg_user => pg_user,
|
33
|
+
:backup_dir => ENV['backup_dir'],
|
34
|
+
:save => save,
|
35
|
+
:send_to_s3 => send_to_s3,
|
36
|
+
:send_to_shared => send_to_shared,
|
37
|
+
:backup_format => ENV['backup_format'],
|
38
|
+
:use_code => use_code,
|
39
|
+
:move_backups => move_backups,
|
40
|
+
:use_scp => use_scp
|
41
|
+
}
|
42
|
+
if include_env
|
43
|
+
# look for these keys in the ENV variable
|
44
|
+
[:shared_dir, :backup_user, :backup_machine, :pg_host, :pg_port].each do |key|
|
45
|
+
val = ENV[key.to_s]
|
46
|
+
db_info[key] = val if val
|
47
|
+
end
|
48
|
+
end
|
49
|
+
BackupUtility::Postgres.new(db_info)
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(db_info)
|
53
|
+
super(db_info)
|
54
|
+
@db_name = db_info.fetch(:db_name)
|
55
|
+
@pg_user = db_info.fetch(:pg_user, 'postgres')
|
56
|
+
config = ActiveRecord::Base.configurations[Rails.env]
|
57
|
+
if config
|
58
|
+
default_port = config['port']
|
59
|
+
else
|
60
|
+
default_port = nil
|
61
|
+
end
|
62
|
+
@pg_host = db_info.fetch(:pg_host, nil)
|
63
|
+
@pg_port = db_info.fetch(:pg_port, default_port)
|
64
|
+
@backup_user = db_info[:backup_user] || 'backup'
|
65
|
+
@backup_format = db_info[:backup_format] || 'plain'
|
66
|
+
raise "invalid format #{@backup_format}" if !['plain', 'custom', 'tar'].include?(@backup_format)
|
67
|
+
@save = db_info.fetch(:save, true)
|
68
|
+
@last_dst = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_paths(settings = {}, append = nil)
|
72
|
+
if !settings.fetch(:ignore, []).empty? || !settings.fetch(:only, []).empty?
|
73
|
+
name = "partial"
|
74
|
+
settings[:date_format] = '%Y-%m-%d-%H-%M'
|
75
|
+
else
|
76
|
+
name = "full"
|
77
|
+
end
|
78
|
+
name += "_#{append}" if append
|
79
|
+
super(settings, name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def pg_dump_command
|
83
|
+
cmd = "#{BIN}/pg_dump -F#{@backup_format} -U #{@pg_user}"
|
84
|
+
cmd += " -h #{@pg_host}" if @pg_host
|
85
|
+
cmd += " -p #{@pg_port}" if @pg_port
|
86
|
+
cmd
|
87
|
+
end
|
88
|
+
|
89
|
+
def custom?
|
90
|
+
@backup_format == 'custom'
|
91
|
+
end
|
92
|
+
|
93
|
+
def dump_schema(working_dir)
|
94
|
+
_dump(working_dir, '--schema-only', '-schema')
|
95
|
+
end
|
96
|
+
|
97
|
+
def dump_data(working_dir, ignore = [])
|
98
|
+
ignore_string = custom? ? '' : '--data-only'
|
99
|
+
ignore.each {|i| ignore_string += ' -T '+i} if ignore.size > 0
|
100
|
+
_dump(working_dir, ignore_string)
|
101
|
+
end
|
102
|
+
|
103
|
+
def dump_data_only(working_dir, only = [])
|
104
|
+
only_string = custom? ? '' : '--data-only'
|
105
|
+
only.each {|o| only_string += ' -t '+o} if only.size > 0
|
106
|
+
label = "-table-" + only.join('-')
|
107
|
+
_dump(working_dir, only_string, label)
|
108
|
+
end
|
109
|
+
|
110
|
+
def _dump(working_dir, append_string = '', label='')
|
111
|
+
ext = custom? ? 'dump' : 'sql'
|
112
|
+
dst = File.join(working_dir, "#{@db_name}#{label}.#{ext}.gz")
|
113
|
+
log_and_time("dumping data (format #{@backup_format}) to #{dst}") do
|
114
|
+
cmd = "#{pg_dump_command} #{append_string} #{@db_name} | gzip > #{dst}"
|
115
|
+
@last_dst = dst
|
116
|
+
`#{cmd}`
|
117
|
+
end
|
118
|
+
File.exists?(dst)
|
119
|
+
end
|
120
|
+
|
121
|
+
def perform_dump(settings, working_dir)
|
122
|
+
# if the backup format is custom, do a full dump since we can do a bunch
|
123
|
+
# of stuff with pg_restore, which means we don't need the individual dumps
|
124
|
+
ignore = settings[:ignore] || []
|
125
|
+
only = settings[:only]
|
126
|
+
if custom?
|
127
|
+
if !only.nil? && !only.empty?
|
128
|
+
return dump_data_only(working_dir, only)
|
129
|
+
else
|
130
|
+
return dump_data(working_dir, ignore)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
if !settings[:skip_schema]
|
134
|
+
if !dump_schema(working_dir)
|
135
|
+
log("failed to dump schema")
|
136
|
+
false
|
137
|
+
end
|
138
|
+
else
|
139
|
+
log("skipping schema dump")
|
140
|
+
end
|
141
|
+
separate = settings[:separate]
|
142
|
+
if separate
|
143
|
+
# first dump the database without any of the separate tables
|
144
|
+
log("dumping database without separate tables")
|
145
|
+
if !dump_data(working_dir, separate + ignore)
|
146
|
+
false
|
147
|
+
end
|
148
|
+
separate.each do |table|
|
149
|
+
log("dumping data only for table #{table}")
|
150
|
+
if !dump_data_only(working_dir, [table])
|
151
|
+
log("failed to dump individual table #{table}")
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
true
|
156
|
+
elsif !only.nil? && !only.empty?
|
157
|
+
dump_data_only(working_dir, only)
|
158
|
+
else
|
159
|
+
dump_data(working_dir, ignore)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def select_cmd(table, where, label)
|
164
|
+
"select * from #{table} where #{where} ORDER BY #{label} ASC"
|
165
|
+
end
|
166
|
+
|
167
|
+
def psql_dump_cmd(cmd, dump_file)
|
168
|
+
cmd = "#{BIN}/psql -c \"#{cmd}\" -F '"
|
169
|
+
cmd += 9.chr
|
170
|
+
cmd += "' -A -d #{@db_name} -U #{@pg_user} -t | gzip > #{dump_file}"
|
171
|
+
cmd
|
172
|
+
end
|
173
|
+
|
174
|
+
def dump_table_by_id(table, id, working_dir)
|
175
|
+
where = "id > #{id}"
|
176
|
+
cmd = select_cmd(table, where, 'id')
|
177
|
+
dump_file = File.join(working_dir, "dump_#{table}_#{id}.sql.gz")
|
178
|
+
dump_cmd = psql_dump_cmd(cmd, dump_file)
|
179
|
+
log("dumping table by id to #{dump_file}")
|
180
|
+
`#{dump_cmd}`
|
181
|
+
ret = $?
|
182
|
+
if !File.exists?(dump_file)
|
183
|
+
log("could not create dump #{dump_file}")
|
184
|
+
false
|
185
|
+
end
|
186
|
+
true
|
187
|
+
end
|
188
|
+
|
189
|
+
def determine_label(table)
|
190
|
+
if table =~ /hour/
|
191
|
+
'hour'
|
192
|
+
elsif table =~ /month/
|
193
|
+
'month'
|
194
|
+
else
|
195
|
+
'day'
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def dump_daily_data(table, day, dump_dir)
|
200
|
+
format = '%m-%d-%Y'
|
201
|
+
start_str = day.strftime(format)
|
202
|
+
# determine the label by the table
|
203
|
+
label = determine_label(table)
|
204
|
+
end_str = (day + 1.day).strftime(format)
|
205
|
+
cmd = select_cmd(table, "#{label} >= '#{start_str}' AND #{label} < '#{end_str}'", label)
|
206
|
+
dump_name = "#{start_str}.sql.gz"
|
207
|
+
dump_file = File.join(dump_dir, dump_name)
|
208
|
+
# if the file exists, skip it as it appears to be already dumped
|
209
|
+
if File.exists?(dump_file)
|
210
|
+
log("dump file #{dump_file} already created, skipping")
|
211
|
+
return false
|
212
|
+
end
|
213
|
+
log("dumping stats to #{dump_file}")
|
214
|
+
dump_cmd = psql_dump_cmd(cmd, dump_file)
|
215
|
+
out = `#{dump_cmd}`
|
216
|
+
ret = $?
|
217
|
+
if !File.exists?(dump_file)
|
218
|
+
log("could not create dump #{dump_file}")
|
219
|
+
false
|
220
|
+
end
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
224
|
+
def dump_monthly_data(table, end_date = nil)
|
225
|
+
# the sql directory structure will look like this
|
226
|
+
# BACKUP_DIR
|
227
|
+
# -> monthly-12-2009
|
228
|
+
# -> hits_by_hour_video_geo
|
229
|
+
# -> 12-1.2009.sql.gz
|
230
|
+
# -> 12-2.2009.sql.gz
|
231
|
+
# -> 12-3.2009.sql.gz
|
232
|
+
# then each directory under the monthly will be tarballed,
|
233
|
+
# and uploaded to s3 with the name monthly-12-2009-hits_by_hour_video_geo.tar.gz
|
234
|
+
end_date = Time.now if end_date.blank?
|
235
|
+
# normalize to beginning of month
|
236
|
+
end_date = Time.utc(end_date.year, end_date.month, 1)
|
237
|
+
format = '%m-%Y'
|
238
|
+
start_day = (end_date - 1.month)
|
239
|
+
# normalize to the beginning of month
|
240
|
+
start_day = Time.utc(start_day.year, start_day.month, 1)
|
241
|
+
start_str = start_day.strftime(format)
|
242
|
+
table_dir = File.join(@backup_dir, "monthly-#{start_str}", table)
|
243
|
+
FileUtils.makedirs(table_dir)
|
244
|
+
# now iterate through all the days up till the end_date, and dump each file
|
245
|
+
while start_day < end_date
|
246
|
+
dump_daily_data(table, start_day, table_dir)
|
247
|
+
start_day += 1.day
|
248
|
+
end
|
249
|
+
# once we have the dir, tar ball the entire table dir
|
250
|
+
tar_ball = File.join(@backup_dir, "monthly-#{start_str}-#{table}.tar.gz")
|
251
|
+
`tar czvf #{tar_ball} #{table_dir}`
|
252
|
+
if File.exists?(tar_ball)
|
253
|
+
send_file_to_s3(tar_ball)
|
254
|
+
# now delete the tarball
|
255
|
+
File.delete(tar_ball)
|
256
|
+
else
|
257
|
+
log("tar ball does not exist #{tar_ball}")
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: backup_utility
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Bruce Wang
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-11-02 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Create database dumps and store them in a remote location
|
22
|
+
email: bwang@twistage.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/backup_utility.rb
|
31
|
+
- lib/backup_utility/base.rb
|
32
|
+
- lib/backup_utility/file.rb
|
33
|
+
- lib/backup_utility/mongo.rb
|
34
|
+
- lib/backup_utility/postgres.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://rubygems.org/gems/backup_utility
|
37
|
+
licenses: []
|
38
|
+
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
segments:
|
50
|
+
- 0
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.7
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Backup Utility
|
67
|
+
test_files: []
|
68
|
+
|