netguru-safe 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.autotest +42 -0
  2. data/.document +5 -0
  3. data/.gitignore +11 -0
  4. data/CHANGELOG +25 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +44 -0
  7. data/LICENSE +20 -0
  8. data/README.markdown +237 -0
  9. data/Rakefile +32 -0
  10. data/TODO +11 -0
  11. data/VERSION +1 -0
  12. data/astrails-safe.gemspec +37 -0
  13. data/bin/astrails-safe +53 -0
  14. data/examples/example_helper.rb +19 -0
  15. data/lib/astrails/safe.rb +73 -0
  16. data/lib/astrails/safe/archive.rb +24 -0
  17. data/lib/astrails/safe/backup.rb +20 -0
  18. data/lib/astrails/safe/cloudfiles.rb +77 -0
  19. data/lib/astrails/safe/config/builder.rb +60 -0
  20. data/lib/astrails/safe/config/node.rb +76 -0
  21. data/lib/astrails/safe/gpg.rb +46 -0
  22. data/lib/astrails/safe/gzip.rb +25 -0
  23. data/lib/astrails/safe/local.rb +51 -0
  24. data/lib/astrails/safe/mongodump.rb +23 -0
  25. data/lib/astrails/safe/mysqldump.rb +32 -0
  26. data/lib/astrails/safe/pgdump.rb +36 -0
  27. data/lib/astrails/safe/pipe.rb +17 -0
  28. data/lib/astrails/safe/s3.rb +75 -0
  29. data/lib/astrails/safe/sftp.rb +88 -0
  30. data/lib/astrails/safe/sink.rb +35 -0
  31. data/lib/astrails/safe/source.rb +47 -0
  32. data/lib/astrails/safe/stream.rb +20 -0
  33. data/lib/astrails/safe/svndump.rb +13 -0
  34. data/lib/astrails/safe/tmp_file.rb +48 -0
  35. data/lib/astrails/safe/version.rb +5 -0
  36. data/lib/extensions/mktmpdir.rb +45 -0
  37. data/spec/integration/airbrake_integration_spec.rb +76 -0
  38. data/spec/integration/archive_integration_spec.rb +88 -0
  39. data/spec/integration/cleanup_spec.rb +61 -0
  40. data/spec/spec.opts +5 -0
  41. data/spec/spec_helper.rb +13 -0
  42. data/spec/unit/archive_spec.rb +67 -0
  43. data/spec/unit/cloudfiles_spec.rb +177 -0
  44. data/spec/unit/config_spec.rb +234 -0
  45. data/spec/unit/gpg_spec.rb +148 -0
  46. data/spec/unit/gzip_spec.rb +64 -0
  47. data/spec/unit/local_spec.rb +110 -0
  48. data/spec/unit/mongodump_spec.rb +54 -0
  49. data/spec/unit/mysqldump_spec.rb +83 -0
  50. data/spec/unit/pgdump_spec.rb +45 -0
  51. data/spec/unit/s3_spec.rb +163 -0
  52. data/spec/unit/svndump_spec.rb +39 -0
  53. data/templates/script.rb +160 -0
  54. metadata +222 -0
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.9.pre
@@ -0,0 +1,37 @@
1
+ require './lib/astrails/safe/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{netguru-safe}
5
+ s.version = Astrails::Safe::VERSION
6
+ s.authors = ["Astrails Ltd."]
7
+ s.date = Time.now.utc.strftime("%Y-%m-%d")
8
+ s.email = %q{we@astrails.com}
9
+ s.homepage = %q{http://blog.astrails.com/astrails-safe}
10
+ s.summary = %q{Backup filesystem and databases (MySQL and PostgreSQL) locally or to a remote server/service (with encryption)}
11
+ s.description = %q{Astrails-Safe is a simple tool to backup databases (MySQL and PostgreSQL), Subversion repositories (with svndump) and just files.
12
+ Backups can be stored locally or remotely and can be enctypted.
13
+ Remote storage is supported on Amazon S3, Rackspace Cloud Files, or just plain SFTP.
14
+ }
15
+
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README.markdown",
19
+ "TODO"
20
+ ]
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files spec`.split("\n")
24
+ s.require_paths = ["lib"]
25
+ s.required_rubygems_version = %q{1.5.0}
26
+ s.default_executable = %q{astrails-safe}
27
+ s.executables = ["astrails-safe"]
28
+
29
+ # tests
30
+ s.add_development_dependency 'rspec', '~> 1.3.2'
31
+ s.add_development_dependency 'rr', '~> 1.0.4'
32
+
33
+ s.add_runtime_dependency 'aws-sdk', '~> 1.2.3'
34
+ s.add_runtime_dependency 'cloudfiles', '~> 1.4.7'
35
+ s.add_runtime_dependency 'net-sftp', '~> 2.0.4'
36
+ s.add_runtime_dependency 'toadhopper', '~> 2.0'
37
+ end
data/bin/astrails-safe ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ # require 'ruby-debug'
6
+ $:.unshift File.expand_path("../../lib", __FILE__)
7
+
8
+ require 'astrails/safe'
9
+ include Astrails::Safe
10
+
11
+ def die(msg)
12
+ puts "ERROR: #{msg}"
13
+ exit 1
14
+ end
15
+
16
+ def usage
17
+ puts <<-END
18
+ Usage: astrails-safe [OPTIONS] CONFIG_FILE
19
+ Options:
20
+ -h, --help This help screen
21
+ -v, --verbose be verbose, duh!
22
+ -n, --dry-run just pretend, don't do anything.
23
+ -L, --local skip S3 and Cloud Files
24
+
25
+ Note: config file will be created from template if missing
26
+ END
27
+ exit 1
28
+ end
29
+
30
+ def process_options
31
+ usage if ARGV.delete("-h") || ARGV.delete("--help")
32
+ $_VERBOSE = ARGV.delete("-v") || ARGV.delete("--verbose")
33
+ $DRY_RUN = ARGV.delete("-n") || ARGV.delete("--dry-run")
34
+ $LOCAL = ARGV.delete("-L") || ARGV.delete("--local")
35
+ usage unless ARGV.first
36
+ $CONFIG_FILE_NAME = File.expand_path(ARGV.first)
37
+ end
38
+
39
+ def main
40
+ process_options
41
+
42
+ unless File.exists?($CONFIG_FILE_NAME)
43
+ die "Missing configuration file. NOT CREATED! Rerun w/o the -n argument to create a template configuration file." if $DRY_RUN
44
+
45
+ FileUtils.cp File.join(Astrails::Safe::ROOT, "templates", "script.rb"), $CONFIG_FILE_NAME
46
+
47
+ die "Created default #{$CONFIG_FILE_NAME}. Please edit and run again."
48
+ end
49
+
50
+ load($CONFIG_FILE_NAME)
51
+ end
52
+
53
+ main
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'micronaut'
3
+ require 'ruby-debug'
4
+
5
+ SAFE_ROOT = File.dirname(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ $LOAD_PATH.unshift(File.join(SAFE_ROOT, 'lib'))
8
+
9
+ require 'astrails/safe'
10
+
11
+ def not_in_editor?
12
+ !(ENV.has_key?('TM_MODE') || ENV.has_key?('EMACS') || ENV.has_key?('VIM'))
13
+ end
14
+
15
+ Micronaut.configure do |c|
16
+ c.color_enabled = not_in_editor?
17
+ c.filter_run :focused => true
18
+ c.mock_with :rr
19
+ end
@@ -0,0 +1,73 @@
1
+ require "aws-sdk"
2
+ require "cloudfiles"
3
+ require 'net/sftp'
4
+ require 'fileutils'
5
+ require 'benchmark'
6
+ require 'toadhopper'
7
+
8
+ require 'tempfile'
9
+ require 'extensions/mktmpdir'
10
+
11
+ require 'astrails/safe/tmp_file'
12
+
13
+ require 'astrails/safe/config/node'
14
+ require 'astrails/safe/config/builder'
15
+
16
+ require 'astrails/safe/stream'
17
+
18
+ require 'astrails/safe/backup'
19
+
20
+ require 'astrails/safe/source'
21
+ require 'astrails/safe/mysqldump'
22
+ require 'astrails/safe/pgdump'
23
+ require 'astrails/safe/archive'
24
+ require 'astrails/safe/svndump'
25
+ require 'astrails/safe/mongodump'
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/s3'
34
+ require 'astrails/safe/cloudfiles'
35
+ require 'astrails/safe/sftp'
36
+
37
+ require 'astrails/safe/version'
38
+
39
+ module Astrails
40
+ module Safe
41
+ ROOT = File.join(File.dirname(__FILE__), "..", "..")
42
+
43
+ def safe(&block)
44
+ config = Config::Node.new(&block)
45
+
46
+ begin
47
+ [[Mysqldump, [:mysqldump, :databases]],
48
+ [Pgdump, [:pgdump, :databases]],
49
+ [Mongodump, [:mongodump, :databases]],
50
+ [Archive, [:tar, :archives]],
51
+ [Svndump, [:svndump, :repos]]
52
+ ].each do |klass, path|
53
+ if collection = config[*path]
54
+ collection.each do |name, config|
55
+ klass.new(name, config).backup.run(config, :gpg, :gzip, :local, :s3, :cloudfiles, :sftp)
56
+ end
57
+ end
58
+ end
59
+ rescue => e
60
+ begin
61
+ if config["airbrake"]
62
+ toad = Toadhopper.new(config["airbrake"]["api_key"])
63
+ toad.post!(e)
64
+ end
65
+ rescue
66
+ end
67
+ ensure
68
+ Astrails::Safe::TmpFile.cleanup
69
+ end
70
+ end
71
+ module_function :safe
72
+ end
73
+ 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}"}.join(" ")
15
+ end
16
+
17
+ def tar_files
18
+ raise RuntimeError, "missing files for tar" unless @config[:files]
19
+ [*@config[:files]].map{|s| s.strip}.join(" ")
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,77 @@
1
+ module Astrails
2
+ module Safe
3
+ class Cloudfiles < Sink
4
+ MAX_CLOUDFILES_FILE_SIZE = 5368709120
5
+
6
+ protected
7
+
8
+ def active?
9
+ container && user && api_key
10
+ end
11
+
12
+ def path
13
+ @path ||= expand(config[:cloudfiles, :path] || config[:local, :path] || ":kind/:id")
14
+ end
15
+
16
+ # UGLY: we need this function for the reason that
17
+ # we can't double mock on ruby 1.9.2, duh!
18
+ # so we created this func to mock it all together
19
+ def get_file_size(path)
20
+ File.stat(path).size
21
+ end
22
+
23
+ def save
24
+ raise RuntimeError, "pipe-streaming not supported for S3." unless @backup.path
25
+
26
+ # needed in cleanup even on dry run
27
+ cf = CloudFiles::Connection.new(user, api_key, true, service_net) unless $LOCAL
28
+ puts "Uploading #{container}:#{full_path} from #{@backup.path}" if $_VERBOSE || $DRY_RUN
29
+ unless $DRY_RUN || $LOCAL
30
+ if get_file_size(@backup.path) > MAX_CLOUDFILES_FILE_SIZE
31
+ STDERR.puts "ERROR: File size exceeds maximum allowed for upload to Cloud Files (#{MAX_CLOUDFILES_FILE_SIZE}): #{@backup.path}"
32
+ return
33
+ end
34
+ benchmark = Benchmark.realtime do
35
+ cf_container = cf.create_container(container)
36
+ o = cf_container.create_object(full_path,true)
37
+ o.write(File.open(@backup.path))
38
+ end
39
+ puts "...done" if $_VERBOSE
40
+ puts("Upload took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
41
+ end
42
+ end
43
+
44
+ def cleanup
45
+ return if $LOCAL
46
+
47
+ return unless keep = @config[:keep, :cloudfiles]
48
+
49
+ puts "listing files: #{container}:#{base}*" if $_VERBOSE
50
+ cf = CloudFiles::Connection.new(user, api_key, true, service_net) unless $LOCAL
51
+ cf_container = cf.container(container)
52
+ files = cf_container.objects(:prefix => base).sort
53
+
54
+ cleanup_with_limit(files, keep) do |f|
55
+ puts "removing Cloud File #{container}:#{f}" if $DRY_RUN || $_VERBOSE
56
+ cf_container.delete_object(f) unless $DRY_RUN || $LOCAL
57
+ end
58
+ end
59
+
60
+ def container
61
+ @config[:cloudfiles, :container]
62
+ end
63
+
64
+ def user
65
+ @config[:cloudfiles, :user]
66
+ end
67
+
68
+ def api_key
69
+ @config[:cloudfiles, :api_key]
70
+ end
71
+
72
+ def service_net
73
+ @config[:cloudfiles, :service_net] || false
74
+ end
75
+ end
76
+ end
77
+ 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 cloudfiles key secret bucket api_key container service_net path gpg password keep local mysqldump pgdump command options
7
+ user host port socket skip_tables tar files exclude filename svndump repo_path sftp mongodump airbrake/
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,76 @@
1
+ require 'astrails/safe/config/builder'
2
+ module Astrails
3
+ module Safe
4
+ module Config
5
+ class Node
6
+ attr_reader :parent
7
+ attr_reader :data
8
+ def initialize(parent = nil, data = {}, &block)
9
+ @parent, @data = parent, {}
10
+ data.each { |k, v| self[k] = v }
11
+ Builder.new(self).instance_eval(&block) if block
12
+ end
13
+
14
+ # looks for the path from this node DOWN. will not delegate to parent
15
+ def get(*path)
16
+ key = path.shift
17
+ value = @data[key.to_s]
18
+ return value if value && path.empty?
19
+
20
+ value && value.get(*path)
21
+ end
22
+
23
+ # recursive find
24
+ # starts at the node and continues to the parent
25
+ def find(*path)
26
+ get(*path) || @parent && @parent.find(*path)
27
+ end
28
+ alias :[] :find
29
+
30
+ MULTIVALUES = %w/skip_tables exclude files/
31
+ def set(key, value, &block)
32
+ if @data[key.to_s]
33
+ raise(ArgumentError, "duplicate value for '#{key}'") if value.is_a?(Hash) || !MULTIVALUES.include?(key.to_s)
34
+ end
35
+
36
+ if value.is_a?(Hash)
37
+ @data[key.to_s] = Node.new(self, value, &block)
38
+ else
39
+ raise(ArgumentError, "#{key}: no block supported for simple values") if block
40
+ if @data[key.to_s]
41
+ @data[key.to_s] = [*@data[key.to_s]] + [value]
42
+ else
43
+ @data[key.to_s] = value
44
+ end
45
+ value
46
+ end
47
+ end
48
+ alias :[]= :set
49
+
50
+ def each(&block)
51
+ @data.each(&block)
52
+ end
53
+ include Enumerable
54
+
55
+ def to_hash
56
+ @data.keys.inject({}) do |res, key|
57
+ value = @data[key]
58
+ res[key] = value.is_a?(Node) ? value.to_hash : value
59
+ res
60
+ end
61
+ end
62
+
63
+ def dump(indent = "")
64
+ @data.each do |key, value|
65
+ if value.is_a?(Node)
66
+ puts "#{indent}#{key}:"
67
+ value.dump(indent + " ")
68
+ else
69
+ puts "#{indent}#{key}: #{value.inspect}"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end