netguru-safe 0.2.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.
- data/.autotest +42 -0
- data/.document +5 -0
- data/.gitignore +11 -0
- data/CHANGELOG +25 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +44 -0
- data/LICENSE +20 -0
- data/README.markdown +237 -0
- data/Rakefile +32 -0
- data/TODO +11 -0
- data/VERSION +1 -0
- data/astrails-safe.gemspec +37 -0
- data/bin/astrails-safe +53 -0
- data/examples/example_helper.rb +19 -0
- data/lib/astrails/safe.rb +73 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/backup.rb +20 -0
- data/lib/astrails/safe/cloudfiles.rb +77 -0
- data/lib/astrails/safe/config/builder.rb +60 -0
- data/lib/astrails/safe/config/node.rb +76 -0
- data/lib/astrails/safe/gpg.rb +46 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +51 -0
- data/lib/astrails/safe/mongodump.rb +23 -0
- data/lib/astrails/safe/mysqldump.rb +32 -0
- data/lib/astrails/safe/pgdump.rb +36 -0
- data/lib/astrails/safe/pipe.rb +17 -0
- data/lib/astrails/safe/s3.rb +75 -0
- data/lib/astrails/safe/sftp.rb +88 -0
- data/lib/astrails/safe/sink.rb +35 -0
- data/lib/astrails/safe/source.rb +47 -0
- data/lib/astrails/safe/stream.rb +20 -0
- data/lib/astrails/safe/svndump.rb +13 -0
- data/lib/astrails/safe/tmp_file.rb +48 -0
- data/lib/astrails/safe/version.rb +5 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/spec/integration/airbrake_integration_spec.rb +76 -0
- data/spec/integration/archive_integration_spec.rb +88 -0
- data/spec/integration/cleanup_spec.rb +61 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/archive_spec.rb +67 -0
- data/spec/unit/cloudfiles_spec.rb +177 -0
- data/spec/unit/config_spec.rb +234 -0
- data/spec/unit/gpg_spec.rb +148 -0
- data/spec/unit/gzip_spec.rb +64 -0
- data/spec/unit/local_spec.rb +110 -0
- data/spec/unit/mongodump_spec.rb +54 -0
- data/spec/unit/mysqldump_spec.rb +83 -0
- data/spec/unit/pgdump_spec.rb +45 -0
- data/spec/unit/s3_spec.rb +163 -0
- data/spec/unit/svndump_spec.rb +39 -0
- data/templates/script.rb +160 -0
- metadata +222 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gpg < Pipe
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def post_process
|
8
|
+
@backup.compressed = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
command = @config[:gpg, :command] || 'gpg'
|
13
|
+
if key
|
14
|
+
"|#{command} #{@config[:gpg, :options]} -e -r #{key}"
|
15
|
+
elsif password
|
16
|
+
"|#{command} #{@config[:gpg, :options]} -c --passphrase-file #{gpg_password_file(password)}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def extension
|
21
|
+
".gpg"
|
22
|
+
end
|
23
|
+
|
24
|
+
def active?
|
25
|
+
raise RuntimeError, "can't use both gpg password and pubkey" if key && password
|
26
|
+
|
27
|
+
!!(password || key)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def password
|
33
|
+
@password ||= @config[:gpg, :password]
|
34
|
+
end
|
35
|
+
|
36
|
+
def key
|
37
|
+
@key ||= @config[:gpg, :key]
|
38
|
+
end
|
39
|
+
|
40
|
+
def gpg_password_file(pass)
|
41
|
+
return "TEMP_GENERATED_FILENAME" if $DRY_RUN
|
42
|
+
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gzip < Pipe
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def post_process
|
8
|
+
@backup.compressed = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
"|gzip"
|
13
|
+
end
|
14
|
+
|
15
|
+
def extension
|
16
|
+
".gz"
|
17
|
+
end
|
18
|
+
|
19
|
+
def active?
|
20
|
+
!@backup.compressed
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Local < Sink
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def active?
|
8
|
+
# S3 can't upload from pipe. it needs to know file size, so we must pass through :local
|
9
|
+
# will change once we add SSH/FTP sink
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def path
|
14
|
+
@path ||= File.expand_path(expand(@config[:local, :path] || raise(RuntimeError, "missing :local/:path")))
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
puts "command: #{@backup.command}" if $_VERBOSE
|
19
|
+
|
20
|
+
# FIXME: probably need to change this to smth like @backup.finalize!
|
21
|
+
@backup.path = full_path # need to do it outside DRY_RUN so that it will be avialable for S3 DRY_RUN
|
22
|
+
|
23
|
+
unless $DRY_RUN
|
24
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
25
|
+
benchmark = Benchmark.realtime do
|
26
|
+
system "#{@backup.command}>#{@backup.path}"
|
27
|
+
end
|
28
|
+
puts("command took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def cleanup
|
34
|
+
return unless keep = @config[:keep, :local]
|
35
|
+
|
36
|
+
puts "listing files #{base}" if $_VERBOSE
|
37
|
+
|
38
|
+
# TODO: cleanup ALL zero-length files
|
39
|
+
|
40
|
+
files = Dir["#{base}*"] .
|
41
|
+
select{|f| File.file?(f) && File.size(f) > 0} .
|
42
|
+
sort
|
43
|
+
|
44
|
+
cleanup_with_limit(files, keep) do |f|
|
45
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
46
|
+
File.unlink(f) unless $DRY_RUN
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mongodump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
opts = []
|
7
|
+
opts << "--host #{@config[:host]}" if @config[:host]
|
8
|
+
opts << "-u #{@config[:user]}" if @config[:user]
|
9
|
+
opts << "-p #{@config[:password]}" if @config[:password]
|
10
|
+
opts << "--out #{output_directory}"
|
11
|
+
|
12
|
+
"mongodump -q \"{xxxx : { \\$ne : 0 } }\" --db #{@id} #{opts.join(" ")} && cd #{output_directory} && tar cf - ."
|
13
|
+
end
|
14
|
+
|
15
|
+
def extension; '.tar'; end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def output_directory
|
19
|
+
File.join(TmpFile.tmproot, "mongodump")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mysqldump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
"mysqldump --defaults-extra-file=#{mysql_password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def extension; '.sql'; end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def mysql_password_file
|
14
|
+
Astrails::Safe::TmpFile.create("mysqldump") do |file|
|
15
|
+
file.puts "[mysqldump]"
|
16
|
+
%w/user password socket host port/.each do |k|
|
17
|
+
v = @config[k]
|
18
|
+
# values are quoted if needed
|
19
|
+
file.puts "#{k} = #{v.inspect}" if v
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def mysql_skip_tables
|
25
|
+
if skip_tables = @config[:skip_tables]
|
26
|
+
[*skip_tables].map{ |t| "--ignore-table=#{@id}.#{t}" }.join(" ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Pgdump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
if @config["password"]
|
7
|
+
ENV['PGPASSWORD'] = @config["password"]
|
8
|
+
else
|
9
|
+
ENV['PGPASSWORD'] = nil
|
10
|
+
end
|
11
|
+
"pg_dump #{postgres_options} #{postgres_username} #{postgres_host} #{postgres_port} #{@id}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def extension; '.sql'; end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def postgres_options
|
19
|
+
@config[:options]
|
20
|
+
end
|
21
|
+
|
22
|
+
def postgres_host
|
23
|
+
@config["host"] && "--host='#{@config["host"]}'"
|
24
|
+
end
|
25
|
+
|
26
|
+
def postgres_port
|
27
|
+
@config["port"] && "--port='#{@config["port"]}'"
|
28
|
+
end
|
29
|
+
|
30
|
+
def postgres_username
|
31
|
+
@config["user"] && "--username='#{@config["user"]}'"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Pipe < Stream
|
4
|
+
# process adds required commands to the current
|
5
|
+
# shell command string
|
6
|
+
# :active?, :pipe, :extension and :post_process are
|
7
|
+
# defined in inheriting pipe classes
|
8
|
+
def process
|
9
|
+
return unless active?
|
10
|
+
|
11
|
+
@backup.command << pipe
|
12
|
+
@backup.extension << extension
|
13
|
+
post_process
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class S3 < Sink
|
4
|
+
MAX_S3_FILE_SIZE = 5368709120
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def active?
|
9
|
+
bucket && key && secret
|
10
|
+
end
|
11
|
+
|
12
|
+
def path
|
13
|
+
@path ||= expand(config[:s3, :path] || config[:local, :path] || ":kind/:id")
|
14
|
+
end
|
15
|
+
|
16
|
+
def save
|
17
|
+
# FIXME: user friendly error here :)
|
18
|
+
raise RuntimeError, "pipe-streaming not supported for S3." unless @backup.path
|
19
|
+
|
20
|
+
puts "Uploading #{bucket}:#{full_path}" if $_VERBOSE || $DRY_RUN
|
21
|
+
unless $DRY_RUN || $LOCAL
|
22
|
+
if File.stat(@backup.path).size > MAX_S3_FILE_SIZE
|
23
|
+
STDERR.puts "ERROR: File size exceeds maximum allowed for upload to S3 (#{MAX_S3_FILE_SIZE}): #{@backup.path}"
|
24
|
+
return
|
25
|
+
end
|
26
|
+
benchmark = Benchmark.realtime do
|
27
|
+
File.open(@backup.path) do |file|
|
28
|
+
remote_bucket.objects.create(full_path, :data => file)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
puts "...done" if $_VERBOSE
|
32
|
+
puts("Upload took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def cleanup
|
37
|
+
return if $LOCAL
|
38
|
+
|
39
|
+
return unless keep = @config[:keep, :s3]
|
40
|
+
|
41
|
+
puts "listing files: #{bucket}:#{base}*" if $_VERBOSE
|
42
|
+
files = remote_bucket.objects.with_prefix(:prefix => base)
|
43
|
+
puts files.collect {|x| x.key} if $_VERBOSE
|
44
|
+
|
45
|
+
files = files.sort { |x,y| x.key <=> y.key }
|
46
|
+
|
47
|
+
cleanup_with_limit(files, keep) do |f|
|
48
|
+
puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
|
49
|
+
f.delete unless $DRY_RUN || $LOCAL
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def remote_bucket
|
54
|
+
unless @remote_bucket
|
55
|
+
s3 = AWS::S3.new(:access_key_id => key, :secret_access_key => secret)
|
56
|
+
@remote_bucket = s3.buckets.create(bucket)
|
57
|
+
end
|
58
|
+
@remote_bucket
|
59
|
+
end
|
60
|
+
|
61
|
+
def bucket
|
62
|
+
@config[:s3, :bucket]
|
63
|
+
end
|
64
|
+
|
65
|
+
def key
|
66
|
+
@config[:s3, :key]
|
67
|
+
end
|
68
|
+
|
69
|
+
def secret
|
70
|
+
@config[:s3, :secret]
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Sftp < Sink
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def active?
|
8
|
+
host && user
|
9
|
+
end
|
10
|
+
|
11
|
+
def path
|
12
|
+
@path ||= expand(config[:sftp, :path] || config[:local, :path] || ":kind/:id")
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
raise RuntimeError, "pipe-streaming not supported for SFTP." unless @backup.path
|
17
|
+
|
18
|
+
puts "Uploading #{host}:#{full_path} via SFTP" if $_VERBOSE || $DRY_RUN
|
19
|
+
|
20
|
+
unless $DRY_RUN || $LOCAL
|
21
|
+
opts = {}
|
22
|
+
opts[:password] = password if password
|
23
|
+
opts[:port] = port if port
|
24
|
+
Net::SFTP.start(host, user, opts) do |sftp|
|
25
|
+
puts "Sending #{@backup.path} to #{full_path}" if $_VERBOSE
|
26
|
+
begin
|
27
|
+
sftp.upload! @backup.path, full_path
|
28
|
+
rescue Net::SFTP::StatusException
|
29
|
+
puts "Ensuring remote path (#{path}) exists" if $_VERBOSE
|
30
|
+
# mkdir -p
|
31
|
+
folders = path.split('/')
|
32
|
+
folders.each_index do |i|
|
33
|
+
folder = folders[0..i].join('/')
|
34
|
+
puts "Creating #{folder} on remote" if $_VERBOSE
|
35
|
+
sftp.mkdir!(folder) rescue Net::SFTP::StatusException
|
36
|
+
end
|
37
|
+
retry
|
38
|
+
end
|
39
|
+
end
|
40
|
+
puts "...done" if $_VERBOSE
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def cleanup
|
45
|
+
return if $LOCAL || $DRY_RUN
|
46
|
+
|
47
|
+
return unless keep = @config[:keep, :sftp]
|
48
|
+
|
49
|
+
puts "listing files: #{host}:#{base}*" if $_VERBOSE
|
50
|
+
opts = {}
|
51
|
+
opts[:password] = password if password
|
52
|
+
opts[:port] = port if port
|
53
|
+
Net::SFTP.start(host, user, opts) do |sftp|
|
54
|
+
files = sftp.dir.glob(path, File.basename("#{base}*"))
|
55
|
+
|
56
|
+
puts files.collect {|x| x.name } if $_VERBOSE
|
57
|
+
|
58
|
+
files = files.
|
59
|
+
collect {|x| x.name }.
|
60
|
+
sort
|
61
|
+
|
62
|
+
cleanup_with_limit(files, keep) do |f|
|
63
|
+
file = File.join(path, f)
|
64
|
+
puts "removing sftp file #{host}:#{file}" if $DRY_RUN || $_VERBOSE
|
65
|
+
sftp.remove!(file) unless $DRY_RUN || $LOCAL
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def host
|
71
|
+
@config[:sftp, :host]
|
72
|
+
end
|
73
|
+
|
74
|
+
def user
|
75
|
+
@config[:sftp, :user]
|
76
|
+
end
|
77
|
+
|
78
|
+
def password
|
79
|
+
@config[:sftp, :password]
|
80
|
+
end
|
81
|
+
|
82
|
+
def port
|
83
|
+
@config[:sftp, :port]
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Sink < Stream
|
4
|
+
|
5
|
+
def process
|
6
|
+
return unless active?
|
7
|
+
|
8
|
+
save
|
9
|
+
cleanup
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
# path is defined in subclass
|
15
|
+
# base is used in 'cleanup' to find all files that begin with base. the '.'
|
16
|
+
# at the end is essential to distinguish b/w foo.* and foobar.* archives for example
|
17
|
+
def base
|
18
|
+
@base ||= File.join(path, File.basename(@backup.filename).split(".").first + '.')
|
19
|
+
end
|
20
|
+
|
21
|
+
def full_path
|
22
|
+
@full_path ||= File.join(path, @backup.filename) + @backup.extension
|
23
|
+
end
|
24
|
+
|
25
|
+
# call block on files to be removed (all except for the LAST 'limit' files
|
26
|
+
def cleanup_with_limit(files, limit, &block)
|
27
|
+
return unless files.size > limit
|
28
|
+
|
29
|
+
to_remove = files[0..(files.size - limit - 1)]
|
30
|
+
# TODO: validate here
|
31
|
+
to_remove.each(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Source < Stream
|
4
|
+
|
5
|
+
attr_accessor :id
|
6
|
+
def initialize(id, config)
|
7
|
+
@id, @config = id.to_s, config
|
8
|
+
end
|
9
|
+
|
10
|
+
def timestamp
|
11
|
+
Time.now.strftime("%y%m%d-%H%M")
|
12
|
+
end
|
13
|
+
|
14
|
+
def kind
|
15
|
+
self.class.human_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def filename
|
19
|
+
@filename ||= expand(":kind-:id.:timestamp")
|
20
|
+
end
|
21
|
+
|
22
|
+
def backup
|
23
|
+
return @backup if @backup
|
24
|
+
@backup = Backup.new(
|
25
|
+
:id => @id,
|
26
|
+
:kind => kind,
|
27
|
+
:extension => extension,
|
28
|
+
:command => command,
|
29
|
+
:timestamp => timestamp
|
30
|
+
)
|
31
|
+
# can't do this in the initializer hash above since
|
32
|
+
# filename() calls expand() which requires @backup
|
33
|
+
# FIXME: move expansion to the backup (last step in ctor) assign :tags here
|
34
|
+
@backup.filename = filename
|
35
|
+
@backup
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def self.human_name
|
41
|
+
name.split('::').last.downcase
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|