astrails-safe 0.0.4 → 0.0.6
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/bin/astrails-safe +11 -321
- data/safe.gemspec +3 -3
- metadata +2 -2
data/bin/astrails-safe
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
|
4
4
|
require 'tempfile'
|
5
5
|
require 'rubygems'
|
6
6
|
require 'fileutils'
|
7
7
|
require "aws/s3"
|
8
8
|
require 'yaml'
|
9
|
+
|
9
10
|
#require 'ruby-debug'
|
11
|
+
#$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
12
|
+
|
13
|
+
require 'astrails/safe'
|
14
|
+
include Astrails::Safe
|
10
15
|
|
11
16
|
def die(msg)
|
12
17
|
puts "ERROR: #{msg}"
|
@@ -27,343 +32,28 @@ END
|
|
27
32
|
exit 1
|
28
33
|
end
|
29
34
|
|
30
|
-
def now
|
31
|
-
@now ||= Time.now
|
32
|
-
end
|
33
|
-
|
34
|
-
def timestamp
|
35
|
-
@timestamp ||= now.strftime("%y%m%d-%H%M")
|
36
|
-
end
|
37
|
-
|
38
|
-
# We allow to put configuration keys on any level
|
39
|
-
class ConfigHash
|
40
|
-
def initialize(root, *path)
|
41
|
-
path = [*path] unless path.empty?
|
42
|
-
# sequence of all the config levels on path given
|
43
|
-
# if at some point there is no value, @config will contain nil for this element and all the following
|
44
|
-
@configs = path.inject([root]) { |res, x| res << (res.last && res.last[x]) }.reverse
|
45
|
-
# if the first element (last before revers) is nil -> the required path does not exist
|
46
|
-
@configs = [{}] if @configs.first.nil?
|
47
|
-
end
|
48
|
-
|
49
|
-
def [](key)
|
50
|
-
conf = @configs.find {|x| x.is_a?(Hash) && x[key]}
|
51
|
-
conf && conf[key]
|
52
|
-
end
|
53
|
-
|
54
|
-
def keys
|
55
|
-
@configs.first.keys
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
def create_config_file(path)
|
61
|
-
File.open(path, "w") do |conf|
|
62
|
-
conf.write <<-CONF
|
63
|
-
# you can use comments
|
64
|
-
|
65
|
-
# Note: keys defined on a deeper level will override upper level keys
|
66
|
-
# See :path for example
|
67
|
-
|
68
|
-
# global path
|
69
|
-
:path: /backup
|
70
|
-
|
71
|
-
## uncomment to enable uploads to Amazon S3
|
72
|
-
## Amazon S3 auth (optional)
|
73
|
-
# :s3_key: YOUR_S3_KEY
|
74
|
-
# :s3_secret: YOUR_S3_SECRET
|
75
|
-
# :s3_bucket: S3_BUCKET
|
76
|
-
|
77
|
-
## uncomment to enable backup rotation. keep only given number of latest
|
78
|
-
## backups. remove the rest
|
79
|
-
#:keep_local: 50
|
80
|
-
#:keep_s3: 200
|
81
|
-
|
82
|
-
:mysql:
|
83
|
-
# local path override for mysql
|
84
|
-
:path: /backup/mysql
|
85
|
-
:socket: /var/run/mysqld/mysqld.sock
|
86
|
-
:mysqldump_options: -ceKq --single-transaction --create-options
|
87
|
-
:username: MYSQL_USER
|
88
|
-
:password: MYSQL_PASS
|
89
|
-
|
90
|
-
:databases:
|
91
|
-
|
92
|
-
:production_db:
|
93
|
-
:skip_tables:
|
94
|
-
- logged_exceptions
|
95
|
-
- request_logs
|
96
|
-
:gpg_password: OPTIONAL_PASSWORD_TO_ENCRYPT
|
97
|
-
|
98
|
-
:tar:
|
99
|
-
:path: /backup/archives
|
100
|
-
:archives:
|
101
|
-
:git_repositories:
|
102
|
-
:files:
|
103
|
-
- /home/git/repositories
|
104
|
-
- /home/mirrors/foo/repositories
|
105
|
-
:exclude:
|
106
|
-
- /home/mirrors/foo/repositories/junk
|
107
|
-
:etc:
|
108
|
-
:files:
|
109
|
-
- /etc
|
110
|
-
|
111
|
-
CONF
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
35
|
def process_options
|
116
36
|
usage if ARGV.delete("-h") || ARGV.delete("--help")
|
117
|
-
$
|
37
|
+
$_VERBOSE = ARGV.delete("-v") || ARGV.delete("--verbose")
|
118
38
|
$DRY_RUN = ARGV.delete("-n") || ARGV.delete("--dry-run")
|
119
39
|
$LOCAL = ARGV.delete("-L") || ARGV.delete("--local")
|
120
40
|
usage unless ARGV.first
|
121
41
|
$CONFIG_FILE_NAME = File.expand_path(ARGV.first)
|
122
42
|
end
|
123
43
|
|
124
|
-
$KEEP_FILES = []
|
125
|
-
|
126
|
-
def create_temp_file(name)
|
127
|
-
file = Tempfile.new("mysqldump", $TMPDIR)
|
128
|
-
|
129
|
-
yield file
|
130
|
-
|
131
|
-
file.close
|
132
|
-
$KEEP_FILES << file # so that it will not get gcollected and removed from filesystem until the end
|
133
|
-
file.path
|
134
|
-
end
|
135
|
-
|
136
|
-
def create_mysql_password_file(conf)
|
137
|
-
create_temp_file("mysqldump") do |file|
|
138
|
-
username = conf[:username]
|
139
|
-
password = conf[:password]
|
140
|
-
socket = conf[:socket]
|
141
|
-
host = conf[:host]
|
142
|
-
port = conf[:port]
|
143
|
-
|
144
|
-
file.puts "[mysqldump]"
|
145
|
-
file.puts "user = #{username}" if username
|
146
|
-
file.puts "password = #{password}" if password
|
147
|
-
file.puts "socket = #{socket}" if socket
|
148
|
-
file.puts "host = #{host}" if host
|
149
|
-
file.puts "port = #{port}" if port
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def create_gpg_password_file(pass)
|
154
|
-
create_temp_file("gpg-pass") { |file| file.write(pass) }
|
155
|
-
end
|
156
|
-
|
157
|
-
|
158
|
-
def mysql_skip_tables(conf, db, cmd)
|
159
|
-
if skip_tables = conf[:skip_tables]
|
160
|
-
skip_tables.each do |t|
|
161
|
-
cmd << "--ignore-table=#{db}.#{t} "
|
162
|
-
end
|
163
|
-
end
|
164
|
-
cmd
|
165
|
-
end
|
166
|
-
|
167
|
-
def mysqldump_extra_options(conf, cmd)
|
168
|
-
cmd << conf[:mysqldump_options] << " " if conf[:mysqldump_options]
|
169
|
-
cmd
|
170
|
-
end
|
171
|
-
def mysqldump(conf, db)
|
172
|
-
cmd = "mysqldump --defaults-extra-file=#{create_mysql_password_file(conf)} "
|
173
|
-
cmd = mysqldump_extra_options(conf, cmd)
|
174
|
-
cmd = mysql_skip_tables(conf, db, cmd)
|
175
|
-
cmd << " #{db} "
|
176
|
-
|
177
|
-
path = conf[:path]
|
178
|
-
die "missing :path in configuration" unless path
|
179
|
-
backup_filename = File.join(path, "mysql-#{db}.#{timestamp}.sql")
|
180
|
-
|
181
|
-
[cmd, backup_filename]
|
182
|
-
end
|
183
|
-
|
184
|
-
def tar_extra_options(conf, cmd)
|
185
|
-
cmd << conf[:tar_options] << " " if conf[:tar_options]
|
186
|
-
cmd
|
187
|
-
end
|
188
|
-
def tar_exclude_files(conf, cmd)
|
189
|
-
if exclude = conf[:exclude]
|
190
|
-
cmd << exclude.map{|x| "--exclude=#{x} "}.join
|
191
|
-
end
|
192
|
-
cmd
|
193
|
-
end
|
194
|
-
|
195
|
-
def tar_files(conf, cmd)
|
196
|
-
die "missing files for tar" unless conf[:files]
|
197
|
-
cmd << conf[:files] * " "
|
198
|
-
end
|
199
|
-
|
200
|
-
def tar_archive(conf, arch)
|
201
|
-
cmd = "tar -cf - "
|
202
|
-
cmd = tar_extra_options(conf, cmd)
|
203
|
-
cmd = tar_exclude_files(conf, cmd)
|
204
|
-
cmd = tar_files(conf, cmd)
|
205
|
-
|
206
|
-
path = conf[:path]
|
207
|
-
die "missing :path in configuration" unless path
|
208
|
-
backup_filename = File.join(path, "archive-#{arch}.#{timestamp}.tar")
|
209
|
-
|
210
|
-
[cmd, backup_filename]
|
211
|
-
end
|
212
|
-
|
213
|
-
# GPG uses compression too :)
|
214
|
-
def compress(conf, cmd, backup_filename)
|
215
|
-
|
216
|
-
gpg_pass = conf[:gpg_password]
|
217
|
-
gpg_key = conf[:gpg_public_key]
|
218
|
-
|
219
|
-
if gpg_key
|
220
|
-
die "can't sue both password and pubkey" if gpg_pass
|
221
|
-
cmd << "|gpg -e -r #{gpg_key}"
|
222
|
-
backup_filename << ".gpg"
|
223
|
-
elsif gpg_pass
|
224
|
-
unless $DRY_RUN
|
225
|
-
cmd << "|gpg -c --passphrase-file #{create_gpg_password_file(gpg_pass)}"
|
226
|
-
else
|
227
|
-
cmd << "|gpg -c --passphrase-file TEMP_GENERATED_FILENAME"
|
228
|
-
end
|
229
|
-
backup_filename << ".gpg"
|
230
|
-
else
|
231
|
-
cmd << "|gzip"
|
232
|
-
backup_filename << ".gz"
|
233
|
-
end
|
234
|
-
[cmd, backup_filename]
|
235
|
-
end
|
236
|
-
|
237
|
-
def timestamped_path(prefix, filename, date)
|
238
|
-
File.join(prefix, "%04d" % date.year, "%02d" % date.month, "%02d" % date.day, File.basename(filename))
|
239
|
-
end
|
240
|
-
|
241
|
-
def cleanup_files(files, limit, &block)
|
242
|
-
return unless files.size > limit
|
243
|
-
|
244
|
-
to_remove = files[0..(files.size - limit - 1)]
|
245
|
-
to_remove.each(&block)
|
246
|
-
end
|
247
|
-
|
248
|
-
def cleanup_local(conf, backup_filename)
|
249
|
-
return unless keep_local = conf[:keep_local]
|
250
|
-
|
251
|
-
dir = File.dirname(backup_filename)
|
252
|
-
base = File.basename(backup_filename).split(".").first
|
253
|
-
|
254
|
-
files = Dir[File.join(dir, "#{base}*")].
|
255
|
-
select{|f| File.file?(f)}.
|
256
|
-
sort
|
257
|
-
|
258
|
-
cleanup_files(files, keep_local) do |f|
|
259
|
-
puts "removing local file #{f}" if $DRY_RUN || $VERBOSE
|
260
|
-
File.unlink(f) unless $DRY_RUN
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
class String
|
265
|
-
def starts_with?(str)
|
266
|
-
self[0..(str.length - 1)] == str
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
def cleanup_s3(conf, bucket, prefix, backup_filename)
|
271
|
-
|
272
|
-
return unless keep_s3 = conf[:keep_s3]
|
273
|
-
|
274
|
-
base = File.basename(backup_filename).split(".").first
|
275
|
-
|
276
|
-
puts "listing files in #{bucket}:#{prefix}"
|
277
|
-
files = AWS::S3::Bucket.objects(bucket, :prefix => prefix, :max_keys => keep_s3 * 2)
|
278
|
-
puts files.collect(&:key)
|
279
|
-
files = files.
|
280
|
-
collect(&:key).
|
281
|
-
select{|o| File.basename(o).starts_with?(base)}.
|
282
|
-
sort
|
283
|
-
|
284
|
-
cleanup_files(files, keep_s3) do |f|
|
285
|
-
puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $VERBOSE
|
286
|
-
AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
|
291
|
-
def s3_upload(conf, backup_filename, default_path)
|
292
|
-
s3_bucket = conf[:s3_bucket]
|
293
|
-
s3_key = conf[:s3_key]
|
294
|
-
s3_secret = conf[:s3_secret]
|
295
|
-
s3_prefix = conf[:s3_path] || default_path
|
296
|
-
s3_path = timestamped_path(s3_prefix, backup_filename, now)
|
297
|
-
|
298
|
-
return unless s3_bucket && s3_key && s3_secret
|
299
|
-
|
300
|
-
puts "Uploading file #{backup_filename} to #{s3_bucket}/#{s3_path}" if $VERBOSE || $DRY_RUN
|
301
|
-
|
302
|
-
AWS::S3::Base.establish_connection!(:access_key_id => s3_key, :secret_access_key => s3_secret, :use_ssl => true)
|
303
|
-
|
304
|
-
unless $DRY_RUN || $LOCAL
|
305
|
-
AWS::S3::Bucket.create(s3_bucket)
|
306
|
-
AWS::S3::S3Object.store(s3_path, open(backup_filename), s3_bucket)
|
307
|
-
end
|
308
|
-
puts "...done" if $VERBOSE
|
309
|
-
|
310
|
-
cleanup_s3(conf, s3_bucket, s3_prefix, backup_filename)
|
311
|
-
end
|
312
|
-
|
313
|
-
def stream_backup(conf, cmd, backup_name, default_path)
|
314
|
-
# prepare COMPRESS
|
315
|
-
cmd, backup_filename = compress(conf, cmd, backup_name)
|
316
|
-
|
317
|
-
dir = File.dirname(backup_filename)
|
318
|
-
FileUtils.mkdir_p(dir) unless File.directory?(dir) || $DRY_RUN
|
319
|
-
|
320
|
-
# EXECUTE
|
321
|
-
puts "Backup command: #{cmd} > #{backup_filename}" if $DRY_RUN || $VERBOSE
|
322
|
-
system "#{cmd} > #{backup_filename}" unless $DRY_RUN
|
323
|
-
|
324
|
-
# UPLOAD
|
325
|
-
s3_upload(conf, backup_filename, default_path)
|
326
|
-
|
327
|
-
# CLEANUP
|
328
|
-
cleanup_local(conf, backup_filename)
|
329
|
-
end
|
330
|
-
|
331
|
-
def backup_mysql
|
332
|
-
ConfigHash.new($CONFIG, :mysql, :databases).keys.each do |db|
|
333
|
-
puts "Backup database #{db}" if $VERBOSE
|
334
|
-
conf = ConfigHash.new($CONFIG, :mysql, :databases, db)
|
335
|
-
cmd, backup_filename = mysqldump(conf, db)
|
336
|
-
stream_backup(conf, cmd, backup_filename, "mysql/#{db}/")
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
def backup_archives
|
341
|
-
ConfigHash.new($CONFIG, :tar, :archives).keys.each do |arch|
|
342
|
-
puts "Backup archive #{arch}" if $VERBOSE
|
343
|
-
conf = ConfigHash.new($CONFIG, :tar, :archives, arch)
|
344
|
-
|
345
|
-
cmd, backup_filename = tar_archive(conf, arch)
|
346
|
-
stream_backup(conf, cmd, backup_filename, "archives/#{arch}/")
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
44
|
def main
|
351
45
|
process_options
|
352
46
|
|
353
47
|
unless File.exists?($CONFIG_FILE_NAME)
|
354
48
|
die "Missing configuration file. NOT CREATED! Rerun w/o the -n argument to create a template configuration file." if $DRY_RUN
|
355
|
-
create_config_file($CONFIG_FILE_NAME)
|
356
|
-
die "Created default #{$CONFIG_FILE_NAME}. Please edit and run again."
|
357
|
-
end
|
358
49
|
|
359
|
-
|
50
|
+
FileUtils.cp File.join(Astrails::Safe::ROOT, "templates", "script.rb"), $CONFIG_FILE_NAME
|
360
51
|
|
361
|
-
|
362
|
-
|
52
|
+
die "Created default #{$CONFIG_FILE_NAME}. Please edit and run again."
|
53
|
+
end
|
363
54
|
|
364
|
-
backup_mysql
|
365
55
|
|
366
|
-
|
56
|
+
load($CONFIG_FILE_NAME)
|
367
57
|
end
|
368
58
|
|
369
59
|
main
|
data/safe.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "safe"
|
3
|
-
s.version = "0.0.
|
4
|
-
s.date = "2009-03-
|
3
|
+
s.version = "0.0.6"
|
4
|
+
s.date = "2009-03-15"
|
5
5
|
s.summary = "Astrails Safe"
|
6
6
|
s.email = "we@astrails.com"
|
7
7
|
s.homepage = "http://github.com/astrails/safe"
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.files = files = %w(
|
12
12
|
bin/astrails-safe
|
13
13
|
safe.gemspec
|
14
|
-
)
|
14
|
+
) + Dir['lib/**/*'] + Dir['template/**/*']
|
15
15
|
s.executables = files.grep(/^bin/).map {|x| x.gsub(/^bin\//, "")}
|
16
16
|
|
17
17
|
s.test_files = []
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: astrails-safe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Astrails Ltd.
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-03-
|
12
|
+
date: 2009-03-15 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|