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.
Files changed (40) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +211 -0
  3. data/Rakefile +53 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/astrails-safe +53 -0
  6. data/examples/example_helper.rb +19 -0
  7. data/examples/integration/archive_integration_example.rb +86 -0
  8. data/examples/integration/cleanup_example.rb +62 -0
  9. data/examples/unit/archive_example.rb +67 -0
  10. data/examples/unit/config_example.rb +184 -0
  11. data/examples/unit/gpg_example.rb +138 -0
  12. data/examples/unit/gzip_example.rb +64 -0
  13. data/examples/unit/local_example.rb +110 -0
  14. data/examples/unit/mysqldump_example.rb +83 -0
  15. data/examples/unit/pgdump_example.rb +45 -0
  16. data/examples/unit/s3_example.rb +112 -0
  17. data/examples/unit/svndump_example.rb +39 -0
  18. data/lib/astrails/safe.rb +62 -0
  19. data/lib/astrails/safe/archive.rb +24 -0
  20. data/lib/astrails/safe/backup.rb +20 -0
  21. data/lib/astrails/safe/config/builder.rb +60 -0
  22. data/lib/astrails/safe/config/node.rb +66 -0
  23. data/lib/astrails/safe/gpg.rb +45 -0
  24. data/lib/astrails/safe/gzip.rb +25 -0
  25. data/lib/astrails/safe/local.rb +46 -0
  26. data/lib/astrails/safe/mongodbdump.rb +25 -0
  27. data/lib/astrails/safe/multi.rb +134 -0
  28. data/lib/astrails/safe/mysqldump.rb +31 -0
  29. data/lib/astrails/safe/pgdump.rb +36 -0
  30. data/lib/astrails/safe/pipe.rb +13 -0
  31. data/lib/astrails/safe/s3.rb +68 -0
  32. data/lib/astrails/safe/sftp.rb +79 -0
  33. data/lib/astrails/safe/sink.rb +37 -0
  34. data/lib/astrails/safe/source.rb +46 -0
  35. data/lib/astrails/safe/stream.rb +19 -0
  36. data/lib/astrails/safe/svndump.rb +13 -0
  37. data/lib/astrails/safe/tmp_file.rb +48 -0
  38. data/lib/extensions/mktmpdir.rb +45 -0
  39. data/templates/script.rb +130 -0
  40. 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