bostonlogic-safe 0.3.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.
Files changed (41) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +227 -0
  3. data/Rakefile +54 -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/rcloud_example.rb +110 -0
  17. data/examples/unit/s3_example.rb +112 -0
  18. data/examples/unit/svndump_example.rb +39 -0
  19. data/lib/astrails/safe.rb +71 -0
  20. data/lib/astrails/safe/archive.rb +24 -0
  21. data/lib/astrails/safe/backup.rb +20 -0
  22. data/lib/astrails/safe/config/builder.rb +62 -0
  23. data/lib/astrails/safe/config/node.rb +66 -0
  24. data/lib/astrails/safe/gpg.rb +45 -0
  25. data/lib/astrails/safe/gzip.rb +25 -0
  26. data/lib/astrails/safe/local.rb +48 -0
  27. data/lib/astrails/safe/mysqldump.rb +31 -0
  28. data/lib/astrails/safe/notification.rb +66 -0
  29. data/lib/astrails/safe/pgdump.rb +36 -0
  30. data/lib/astrails/safe/pipe.rb +13 -0
  31. data/lib/astrails/safe/rcloud.rb +73 -0
  32. data/lib/astrails/safe/s3.rb +68 -0
  33. data/lib/astrails/safe/sftp.rb +79 -0
  34. data/lib/astrails/safe/sink.rb +33 -0
  35. data/lib/astrails/safe/source.rb +46 -0
  36. data/lib/astrails/safe/stream.rb +19 -0
  37. data/lib/astrails/safe/svndump.rb +13 -0
  38. data/lib/astrails/safe/tmp_file.rb +48 -0
  39. data/lib/extensions/mktmpdir.rb +45 -0
  40. data/templates/script.rb +155 -0
  41. metadata +135 -0
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
2
+
3
+ describe Astrails::Safe::Svndump do
4
+ def def_config
5
+ {
6
+ :options => "OPTS",
7
+ :repo_path => "bar/baz"
8
+ }
9
+ end
10
+
11
+ def svndump(id = :foo, config = def_config)
12
+ Astrails::Safe::Svndump.new(id, Astrails::Safe::Config::Node.new(nil, config))
13
+ end
14
+
15
+ before(:each) do
16
+ stub(Time).now.stub!.strftime {"NOW"}
17
+ end
18
+
19
+ after(:each) { Astrails::Safe::TmpFile.cleanup }
20
+
21
+ describe :backup do
22
+ before(:each) do
23
+ @svn = svndump
24
+ end
25
+
26
+ {
27
+ :id => "foo",
28
+ :kind => "svndump",
29
+ :extension => ".svn",
30
+ :filename => "svndump-foo.NOW",
31
+ :command => "svnadmin dump OPTS bar/baz",
32
+ }.each do |k, v|
33
+ it "should set #{k} to #{v}" do
34
+ @svn.backup.send(k).should == v
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,71 @@
1
+ require "aws/s3"
2
+ require 'cloudfiles'
3
+ require 'net/sftp'
4
+ require 'fileutils'
5
+ require 'benchmark'
6
+ require 'net/smtp'
7
+
8
+ require 'yaml'
9
+
10
+ require 'tempfile'
11
+ require File.dirname(__FILE__) + '/../extensions/mktmpdir'
12
+
13
+ require File.dirname(__FILE__) + '/safe/tmp_file'
14
+
15
+ require File.dirname(__FILE__) + '/safe/config/node'
16
+ require File.dirname(__FILE__) + '/safe/config/builder'
17
+
18
+ require File.dirname(__FILE__) + '/safe/stream'
19
+
20
+ require File.dirname(__FILE__) + '/safe/backup'
21
+
22
+ require File.dirname(__FILE__) + '/safe/backup'
23
+
24
+ require File.dirname(__FILE__) + '/safe/source'
25
+ require File.dirname(__FILE__) + '/safe/mysqldump'
26
+ require File.dirname(__FILE__) + '/safe/pgdump'
27
+ require File.dirname(__FILE__) + '/safe/archive'
28
+ require File.dirname(__FILE__) + '/safe/svndump'
29
+
30
+ require File.dirname(__FILE__) + '/safe/pipe'
31
+ require File.dirname(__FILE__) + '/safe/gpg'
32
+ require File.dirname(__FILE__) + '/safe/gzip'
33
+
34
+ require File.dirname(__FILE__) + '/safe/sink'
35
+ require File.dirname(__FILE__) + '/safe/local'
36
+ require File.dirname(__FILE__) + '/safe/s3'
37
+ require File.dirname(__FILE__) + '/safe/sftp'
38
+
39
+ require File.dirname(__FILE__) + '/safe/rcloud'
40
+ require File.dirname(__FILE__) + '/safe/notification'
41
+
42
+ module Astrails
43
+ module Safe
44
+ ROOT = File.join(File.dirname(__FILE__), "..", "..")
45
+
46
+ def safe(&block)
47
+ config = Config::Node.new(&block)
48
+ #config.dump
49
+
50
+ begin
51
+ [[Mysqldump, [:mysqldump, :databases]],
52
+ [Pgdump, [:pgdump, :databases]],
53
+ [Archive, [:tar, :archives]],
54
+ [Svndump, [:svndump, :repos]]
55
+ ].each do |klass, path|
56
+ if collection = config[*path]
57
+ collection.each do |name, config|
58
+ klass.new(name, config).backup.run(config, :gpg, :gzip, :local, :s3, :sftp, :rcloud)
59
+ end
60
+ end
61
+ end
62
+
63
+ Astrails::Safe::TmpFile.cleanup
64
+ rescue Exception => e
65
+ puts e.to_yaml if $DRY_RUN || $_VERBOSE
66
+ Notification.new(config, e).send_failure
67
+ end
68
+ end
69
+ module_function :safe
70
+ end
71
+ 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
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,62 @@
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 options
7
+ user host port socket skip_tables tar files exclude filename svndump repo_path sftp
8
+ rcloud username api_key container
9
+ notification subject host domain username password authentication port from recipients/
10
+ NAMES = COLLECTIONS + ITEMS
11
+ def initialize(node)
12
+ @node = node
13
+ end
14
+
15
+ # supported args:
16
+ # args = [value]
17
+ # args = [id, data]
18
+ # args = [data]
19
+ # id/value - simple values, data - hash
20
+ def method_missing(sym, *args, &block)
21
+ return super unless NAMES.include?(sym.to_s)
22
+
23
+ # do we have id or value?
24
+ unless args.first.is_a?(Hash)
25
+ id_or_value = args.shift # nil for args == []
26
+ end
27
+
28
+ id_or_value = id_or_value.map {|v| v.to_s} if id_or_value.is_a?(Array)
29
+
30
+ # do we have data hash?
31
+ if data = args.shift
32
+ raise "#{sym}: hash expected: #{data.inspect}" unless data.is_a?(Hash)
33
+ end
34
+
35
+ #puts "#{sym}: args=#{args.inspect}, id_or_value=#{id_or_value}, data=#{data.inspect}, block=#{block.inspect}"
36
+
37
+ raise "#{sym}: unexpected: #{args.inspect}" unless args.empty?
38
+ raise "#{sym}: missing arguments" unless id_or_value || data || block
39
+
40
+ if COLLECTIONS.include?(sym.to_s) && id_or_value
41
+ data ||= {}
42
+ end
43
+
44
+ if !data && !block
45
+ # simple value assignment
46
+ @node[sym] = id_or_value
47
+
48
+ elsif id_or_value
49
+ # collection element with id => create collection node and a subnode in it
50
+ key = sym.to_s + "s"
51
+ collection = @node[key] || @node.set(key, {})
52
+ collection.set(id_or_value, data || {}, &block)
53
+
54
+ else
55
+ # simple subnode
56
+ @node.set(sym, data || {}, &block)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ 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,48 @@
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
+ @backup.path = full_path # need to do it outside DRY_RUN so that it will be avialable for S3 DRY_RUN
21
+
22
+ unless $DRY_RUN
23
+ FileUtils.mkdir_p(path) unless File.directory?(path)
24
+ benchmark = Benchmark.realtime do
25
+ system "#{@backup.command}>#{@backup.path}"
26
+ end
27
+ puts("command took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
28
+ end
29
+
30
+ end
31
+
32
+ def cleanup
33
+ return unless keep = @config[:keep, :local]
34
+
35
+ puts "listing files #{base}" if $_VERBOSE
36
+
37
+ files = Dir["#{base}*"] .
38
+ select{|f| File.file?(f) && File.size(f) > 0} .
39
+ sort
40
+
41
+ cleanup_with_limit(files, keep) do |f|
42
+ puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
43
+ File.unlink(f) unless $DRY_RUN
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
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
+ file.puts "#{k} = #{v}" if v
19
+ end
20
+ end
21
+ end
22
+
23
+ def mysql_skip_tables
24
+ if skip_tables = @config[:skip_tables]
25
+ [*skip_tables].map { |t| "--ignore-table=#{@id}.#{t}" } * " "
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,66 @@
1
+ module Astrails
2
+ module Safe
3
+ class Notification
4
+
5
+ attr_accessor :config, :error
6
+ def initialize(config, error)
7
+ @config, @error = config, error
8
+ end
9
+
10
+ def send_failure
11
+ if valid?
12
+ msg = "Subject: #{subject}\n\n"
13
+ msg += "#{subject} at #{Time.now.strftime('%A, %B %d, %Y %I:%M:%S %p')}\n\n"
14
+ msg += "Exception: #{error.to_s}\n\n"
15
+ msg += "Stack Trace: #{error.backtrace.join("\n")}\n"
16
+ Net::SMTP.start(host, port, domain, username, password, authentication) do |smtp|
17
+ smtp.send_message(msg, from, recipients)
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def valid?
25
+ subject && host && domain && username && password && authentication && port && from && recipients
26
+ end
27
+
28
+ def subject
29
+ @config[:notification, :subject]
30
+ end
31
+
32
+ def host
33
+ @config[:notification, :host]
34
+ end
35
+
36
+ def domain
37
+ @config[:notification, :domain]
38
+ end
39
+
40
+ def username
41
+ @config[:notification, :username]
42
+ end
43
+
44
+ def password
45
+ @config[:notification, :password]
46
+ end
47
+
48
+ def authentication
49
+ @config[:notification, :authentication]
50
+ end
51
+
52
+ def port
53
+ @config[:notification, :port]
54
+ end
55
+
56
+ def from
57
+ @config[:notification, :from]
58
+ end
59
+
60
+ def recipients
61
+ @config[:notification, :recipients]
62
+ end
63
+
64
+ end
65
+ end
66
+ end