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.
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