backup_utility 1.0.0
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.
- 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
|
+
|