webbynode-safe 0.2.5
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/LICENSE +20 -0
- data/README.markdown +211 -0
- data/Rakefile +53 -0
- data/VERSION.yml +4 -0
- data/bin/astrails-safe +53 -0
- data/examples/example_helper.rb +19 -0
- data/examples/integration/archive_integration_example.rb +86 -0
- data/examples/integration/cleanup_example.rb +62 -0
- data/examples/unit/archive_example.rb +67 -0
- data/examples/unit/config_example.rb +184 -0
- data/examples/unit/gpg_example.rb +138 -0
- data/examples/unit/gzip_example.rb +64 -0
- data/examples/unit/local_example.rb +110 -0
- data/examples/unit/mysqldump_example.rb +83 -0
- data/examples/unit/pgdump_example.rb +45 -0
- data/examples/unit/s3_example.rb +112 -0
- data/examples/unit/svndump_example.rb +39 -0
- data/lib/astrails/safe.rb +62 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/backup.rb +20 -0
- data/lib/astrails/safe/config/builder.rb +60 -0
- data/lib/astrails/safe/config/node.rb +66 -0
- data/lib/astrails/safe/gpg.rb +45 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +46 -0
- data/lib/astrails/safe/mongodbdump.rb +25 -0
- data/lib/astrails/safe/multi.rb +134 -0
- data/lib/astrails/safe/mysqldump.rb +31 -0
- data/lib/astrails/safe/pgdump.rb +36 -0
- data/lib/astrails/safe/pipe.rb +13 -0
- data/lib/astrails/safe/s3.rb +68 -0
- data/lib/astrails/safe/sftp.rb +79 -0
- data/lib/astrails/safe/sink.rb +37 -0
- data/lib/astrails/safe/source.rb +46 -0
- data/lib/astrails/safe/stream.rb +19 -0
- data/lib/astrails/safe/svndump.rb +13 -0
- data/lib/astrails/safe/tmp_file.rb +48 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/templates/script.rb +130 -0
- metadata +126 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require "aws/s3"
|
2
|
+
require 'net/sftp'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
require 'tempfile'
|
7
|
+
require 'extensions/mktmpdir'
|
8
|
+
|
9
|
+
require 'astrails/safe/tmp_file'
|
10
|
+
|
11
|
+
require 'astrails/safe/config/node'
|
12
|
+
require 'astrails/safe/config/builder'
|
13
|
+
|
14
|
+
require 'astrails/safe/stream'
|
15
|
+
|
16
|
+
require 'astrails/safe/backup'
|
17
|
+
|
18
|
+
require 'astrails/safe/backup'
|
19
|
+
|
20
|
+
require 'astrails/safe/source'
|
21
|
+
require 'astrails/safe/mongodbdump'
|
22
|
+
require 'astrails/safe/mysqldump'
|
23
|
+
require 'astrails/safe/pgdump'
|
24
|
+
require 'astrails/safe/archive'
|
25
|
+
require 'astrails/safe/svndump'
|
26
|
+
|
27
|
+
require 'astrails/safe/pipe'
|
28
|
+
require 'astrails/safe/gpg'
|
29
|
+
require 'astrails/safe/gzip'
|
30
|
+
|
31
|
+
require 'astrails/safe/sink'
|
32
|
+
require 'astrails/safe/local'
|
33
|
+
require 'astrails/safe/multi'
|
34
|
+
require 'astrails/safe/s3'
|
35
|
+
require 'astrails/safe/sftp'
|
36
|
+
|
37
|
+
module Astrails
|
38
|
+
module Safe
|
39
|
+
ROOT = File.join(File.dirname(__FILE__), "..", "..")
|
40
|
+
|
41
|
+
def safe(&block)
|
42
|
+
config = Config::Node.new(&block)
|
43
|
+
#config.dump
|
44
|
+
|
45
|
+
[[Mysqldump, [:mysqldump, :databases]],
|
46
|
+
[Mongodbdump, [:mongodbdump, :databases]],
|
47
|
+
[Pgdump, [:pgdump, :databases]],
|
48
|
+
[Archive, [:tar, :archives]],
|
49
|
+
[Svndump, [:svndump, :repos]]
|
50
|
+
].each do |klass, path|
|
51
|
+
if collection = config[*path]
|
52
|
+
collection.each do |name, config|
|
53
|
+
klass.new(name, config).backup.run(config, :multi, :gpg, :gzip, :local, :s3, :sftp)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Astrails::Safe::TmpFile.cleanup
|
59
|
+
end
|
60
|
+
module_function :safe
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Archive < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
"tar -cf - #{@config[:options]} #{tar_exclude_files} #{tar_files}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def extension; '.tar'; end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def tar_exclude_files
|
14
|
+
[*@config[:exclude]].compact.map{|x| "--exclude=#{x}"} * " "
|
15
|
+
end
|
16
|
+
|
17
|
+
def tar_files
|
18
|
+
raise RuntimeError, "missing files for tar" unless @config[:files]
|
19
|
+
[*@config[:files]].map {|s| s.strip} * " "
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Backup
|
4
|
+
attr_accessor :id, :kind, :filename, :extension, :command, :compressed, :timestamp, :path, :processed, :multi
|
5
|
+
def initialize(opts = {})
|
6
|
+
opts.each do |k, v|
|
7
|
+
self.send("#{k}=", v)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def run(config, *mods)
|
12
|
+
mods.each do |mod|
|
13
|
+
mod = mod.to_s
|
14
|
+
mod[0] = mod[0..0].upcase
|
15
|
+
Astrails::Safe.const_get(mod).new(config, self).process
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
module Config
|
4
|
+
class Builder
|
5
|
+
COLLECTIONS = %w/database archive repo/
|
6
|
+
ITEMS = %w/s3 key secret bucket path gpg password keep local mysqldump pgdump mongodbdump
|
7
|
+
options user host port socket skip_tables tar files exclude filename svndump repo_path sftp/
|
8
|
+
NAMES = COLLECTIONS + ITEMS
|
9
|
+
def initialize(node)
|
10
|
+
@node = node
|
11
|
+
end
|
12
|
+
|
13
|
+
# supported args:
|
14
|
+
# args = [value]
|
15
|
+
# args = [id, data]
|
16
|
+
# args = [data]
|
17
|
+
# id/value - simple values, data - hash
|
18
|
+
def method_missing(sym, *args, &block)
|
19
|
+
return super unless NAMES.include?(sym.to_s)
|
20
|
+
|
21
|
+
# do we have id or value?
|
22
|
+
unless args.first.is_a?(Hash)
|
23
|
+
id_or_value = args.shift # nil for args == []
|
24
|
+
end
|
25
|
+
|
26
|
+
id_or_value = id_or_value.map {|v| v.to_s} if id_or_value.is_a?(Array)
|
27
|
+
|
28
|
+
# do we have data hash?
|
29
|
+
if data = args.shift
|
30
|
+
raise "#{sym}: hash expected: #{data.inspect}" unless data.is_a?(Hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
#puts "#{sym}: args=#{args.inspect}, id_or_value=#{id_or_value}, data=#{data.inspect}, block=#{block.inspect}"
|
34
|
+
|
35
|
+
raise "#{sym}: unexpected: #{args.inspect}" unless args.empty?
|
36
|
+
raise "#{sym}: missing arguments" unless id_or_value || data || block
|
37
|
+
|
38
|
+
if COLLECTIONS.include?(sym.to_s) && id_or_value
|
39
|
+
data ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
if !data && !block
|
43
|
+
# simple value assignment
|
44
|
+
@node[sym] = id_or_value
|
45
|
+
|
46
|
+
elsif id_or_value
|
47
|
+
# collection element with id => create collection node and a subnode in it
|
48
|
+
key = sym.to_s + "s"
|
49
|
+
collection = @node[key] || @node.set(key, {})
|
50
|
+
collection.set(id_or_value, data || {}, &block)
|
51
|
+
|
52
|
+
else
|
53
|
+
# simple subnode
|
54
|
+
@node.set(sym, data || {}, &block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'astrails/safe/config/builder'
|
2
|
+
module Astrails
|
3
|
+
module Safe
|
4
|
+
module Config
|
5
|
+
class Node
|
6
|
+
attr_reader :parent
|
7
|
+
def initialize(parent = nil, data = {}, &block)
|
8
|
+
@parent, @data = parent, {}
|
9
|
+
data.each { |k, v| self[k] = v }
|
10
|
+
Builder.new(self).instance_eval(&block) if block
|
11
|
+
end
|
12
|
+
|
13
|
+
# looks for the path from this node DOWN. will not delegate to parent
|
14
|
+
def get(*path)
|
15
|
+
key = path.shift
|
16
|
+
value = @data[key.to_s]
|
17
|
+
return value if value && path.empty?
|
18
|
+
|
19
|
+
value && value.get(*path)
|
20
|
+
end
|
21
|
+
|
22
|
+
# recursive find
|
23
|
+
# starts at the node and continues to the parent
|
24
|
+
def find(*path)
|
25
|
+
get(*path) || @parent && @parent.find(*path)
|
26
|
+
end
|
27
|
+
alias :[] :find
|
28
|
+
|
29
|
+
def set(key, value, &block)
|
30
|
+
@data[key.to_s] =
|
31
|
+
if value.is_a?(Hash)
|
32
|
+
Node.new(self, value, &block)
|
33
|
+
else
|
34
|
+
raise(ArgumentError, "#{key}: no block supported for simple values") if block
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
alias :[]= :set
|
39
|
+
|
40
|
+
def each(&block)
|
41
|
+
@data.each(&block)
|
42
|
+
end
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
@data.keys.inject({}) do |res, key|
|
47
|
+
value = @data[key]
|
48
|
+
res[key] = value.is_a?(Node) ? value.to_hash : value
|
49
|
+
res
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def dump(indent = "")
|
54
|
+
@data.each do |key, value|
|
55
|
+
if value.is_a?(Node)
|
56
|
+
puts "#{indent}#{key}:"
|
57
|
+
value.dump(indent + " ")
|
58
|
+
else
|
59
|
+
puts "#{indent}#{key}: #{value.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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
|
+
if key
|
13
|
+
"|gpg #{@config[:gpg, :options]} -e -r #{key}"
|
14
|
+
elsif password
|
15
|
+
"|gpg #{@config[:gpg,:options]} -c --passphrase-file #{gpg_password_file(password)}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def extension
|
20
|
+
".gpg"
|
21
|
+
end
|
22
|
+
|
23
|
+
def active?
|
24
|
+
raise RuntimeError, "can't use both gpg password and pubkey" if key && password
|
25
|
+
|
26
|
+
!!(password || key)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def password
|
32
|
+
@password ||= @config[:gpg, :password]
|
33
|
+
end
|
34
|
+
|
35
|
+
def key
|
36
|
+
@key ||= @config[:gpg, :key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def gpg_password_file(pass)
|
40
|
+
return "TEMP_GENERATED_FILENAME" if $DRY_RUN
|
41
|
+
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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,46 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Local < Sink
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def active?
|
8
|
+
!backup.processed
|
9
|
+
end
|
10
|
+
|
11
|
+
def path
|
12
|
+
@path ||= File.expand_path(expand(@config[:local, :path] || raise(RuntimeError, "missing :local/:path")))
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
puts "command: #{@backup.command}" if $_VERBOSE
|
17
|
+
|
18
|
+
@backup.path = full_path # need to do it outside DRY_RUN so that it will be avialable for S3 DRY_RUN
|
19
|
+
|
20
|
+
unless $DRY_RUN
|
21
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
22
|
+
benchmark = Benchmark.realtime do
|
23
|
+
system "#{@backup.command}>#{@backup.path}"
|
24
|
+
end
|
25
|
+
puts("command took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def cleanup
|
31
|
+
return unless keep = @config[:keep, :local]
|
32
|
+
|
33
|
+
puts "listing files #{base}" if $_VERBOSE
|
34
|
+
|
35
|
+
files = Dir["#{base}*"] .
|
36
|
+
select{|f| File.file?(f) && File.size(f) > 0} .
|
37
|
+
sort
|
38
|
+
|
39
|
+
cleanup_with_limit(files, keep) do |f|
|
40
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
41
|
+
File.unlink(f) unless $DRY_RUN
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mongodbdump < Source
|
4
|
+
def command
|
5
|
+
command = ["mongodump"]
|
6
|
+
command << @config[:options] if @config[:options]
|
7
|
+
command << "--db #{@id}"
|
8
|
+
|
9
|
+
%w[host port out collection].each do |opt|
|
10
|
+
command << "--#{opt} #{@config[opt]}" if @config[opt]
|
11
|
+
end
|
12
|
+
|
13
|
+
command.join(" ")
|
14
|
+
end
|
15
|
+
|
16
|
+
def backup
|
17
|
+
bkp = super
|
18
|
+
bkp.multi = true
|
19
|
+
bkp
|
20
|
+
end
|
21
|
+
|
22
|
+
def extension; '.mongodb.dump.tgz'; end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
module Astrails
|
4
|
+
module Safe
|
5
|
+
class Multi < Sink
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def active?
|
10
|
+
@backup.multi
|
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
|
+
v "=> Backup up multiple files..."
|
19
|
+
v " backup command: #{@backup.command}"
|
20
|
+
|
21
|
+
@backup.path = full_path # need to do it outside DRY_RUN so that it will be avialable for S3 DRY_RUN
|
22
|
+
# @backup.path << ".tgz"
|
23
|
+
|
24
|
+
compression = "tar cf#{$_VERBOSE ? "v" : ""}z #{@backup.path} dump/"
|
25
|
+
v " compression command: #{compression}"
|
26
|
+
|
27
|
+
|
28
|
+
unless $DRY_RUN
|
29
|
+
Dir.tmpdir do
|
30
|
+
v " executing..."
|
31
|
+
v "----"
|
32
|
+
v ""
|
33
|
+
|
34
|
+
out = ">> /dev/null" unless $_VERBOSE
|
35
|
+
benchmark = Benchmark.realtime do
|
36
|
+
system "#{@backup.command}#{out}"
|
37
|
+
system "#{compression}#{out}"
|
38
|
+
end
|
39
|
+
|
40
|
+
v ""
|
41
|
+
v "----"
|
42
|
+
v " command took " + sprintf("%.2f", benchmark) + " second(s)."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@backup.processed = true
|
47
|
+
@backup.compressed = true
|
48
|
+
|
49
|
+
v "=> done!"
|
50
|
+
v ""
|
51
|
+
end
|
52
|
+
|
53
|
+
def cleanup
|
54
|
+
return unless keep = @config[:keep, :local]
|
55
|
+
|
56
|
+
puts "listing files #{base}" if $_VERBOSE
|
57
|
+
|
58
|
+
files = Dir["#{base}*"] .
|
59
|
+
select{|f| File.file?(f) && File.size(f) > 0} .
|
60
|
+
sort
|
61
|
+
|
62
|
+
cleanup_with_limit(files, keep) do |f|
|
63
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
64
|
+
File.unlink(f) unless $DRY_RUN
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Dir
|
72
|
+
module Tmpdir
|
73
|
+
require 'tmpdir'
|
74
|
+
require 'socket'
|
75
|
+
require 'fileutils'
|
76
|
+
|
77
|
+
unless defined?(Super)
|
78
|
+
Super = Dir.send(:method, :tmpdir)
|
79
|
+
class << Dir
|
80
|
+
remove_method :tmpdir
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Error < ::StandardError; end
|
85
|
+
|
86
|
+
Hostname = Socket.gethostname || 'localhost'
|
87
|
+
Pid = Process.pid
|
88
|
+
Ppid = Process.ppid
|
89
|
+
|
90
|
+
def tmpdir *args, &block
|
91
|
+
options = Hash === args.last ? args.pop : {}
|
92
|
+
|
93
|
+
dirname = Super.call(*args)
|
94
|
+
|
95
|
+
return dirname unless block
|
96
|
+
|
97
|
+
turd = options['turd'] || options[:turd]
|
98
|
+
|
99
|
+
basename = [
|
100
|
+
Hostname,
|
101
|
+
Ppid,
|
102
|
+
Pid,
|
103
|
+
Thread.current.object_id.abs,
|
104
|
+
rand
|
105
|
+
].join('-')
|
106
|
+
|
107
|
+
pathname = File.join dirname, basename
|
108
|
+
|
109
|
+
made = false
|
110
|
+
|
111
|
+
42.times do
|
112
|
+
begin
|
113
|
+
FileUtils.mkdir_p pathname
|
114
|
+
break(made = true)
|
115
|
+
rescue Object
|
116
|
+
sleep rand
|
117
|
+
:retry
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
raise Error, "failed to make tmpdir in #{ dirname.inspect }" unless made
|
122
|
+
|
123
|
+
begin
|
124
|
+
return Dir.chdir(pathname, &block)
|
125
|
+
ensure
|
126
|
+
unless turd
|
127
|
+
FileUtils.rm_rf(pathname) if made
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
extend Tmpdir
|
134
|
+
end
|