netguru-safe 0.2.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +42 -0
- data/.document +5 -0
- data/.gitignore +11 -0
- data/CHANGELOG +25 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +44 -0
- data/LICENSE +20 -0
- data/README.markdown +237 -0
- data/Rakefile +32 -0
- data/TODO +11 -0
- data/VERSION +1 -0
- data/astrails-safe.gemspec +37 -0
- data/bin/astrails-safe +53 -0
- data/examples/example_helper.rb +19 -0
- data/lib/astrails/safe.rb +73 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/backup.rb +20 -0
- data/lib/astrails/safe/cloudfiles.rb +77 -0
- data/lib/astrails/safe/config/builder.rb +60 -0
- data/lib/astrails/safe/config/node.rb +76 -0
- data/lib/astrails/safe/gpg.rb +46 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +51 -0
- data/lib/astrails/safe/mongodump.rb +23 -0
- data/lib/astrails/safe/mysqldump.rb +32 -0
- data/lib/astrails/safe/pgdump.rb +36 -0
- data/lib/astrails/safe/pipe.rb +17 -0
- data/lib/astrails/safe/s3.rb +75 -0
- data/lib/astrails/safe/sftp.rb +88 -0
- data/lib/astrails/safe/sink.rb +35 -0
- data/lib/astrails/safe/source.rb +47 -0
- data/lib/astrails/safe/stream.rb +20 -0
- data/lib/astrails/safe/svndump.rb +13 -0
- data/lib/astrails/safe/tmp_file.rb +48 -0
- data/lib/astrails/safe/version.rb +5 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/spec/integration/airbrake_integration_spec.rb +76 -0
- data/spec/integration/archive_integration_spec.rb +88 -0
- data/spec/integration/cleanup_spec.rb +61 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/archive_spec.rb +67 -0
- data/spec/unit/cloudfiles_spec.rb +177 -0
- data/spec/unit/config_spec.rb +234 -0
- data/spec/unit/gpg_spec.rb +148 -0
- data/spec/unit/gzip_spec.rb +64 -0
- data/spec/unit/local_spec.rb +110 -0
- data/spec/unit/mongodump_spec.rb +54 -0
- data/spec/unit/mysqldump_spec.rb +83 -0
- data/spec/unit/pgdump_spec.rb +45 -0
- data/spec/unit/s3_spec.rb +163 -0
- data/spec/unit/svndump_spec.rb +39 -0
- data/templates/script.rb +160 -0
- 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
|