markmansour-safe 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +171 -0
- data/Rakefile +52 -0
- data/VERSION.yml +4 -0
- data/bin/astrails-safe +59 -0
- data/examples/example_helper.rb +19 -0
- data/examples/unit/config_example.rb +175 -0
- data/examples/unit/stream_example.rb +33 -0
- data/lib/astrails/safe.rb +43 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/config/builder.rb +60 -0
- data/lib/astrails/safe/config/node.rb +67 -0
- data/lib/astrails/safe/gpg.rb +42 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +54 -0
- data/lib/astrails/safe/mysqldump.rb +31 -0
- data/lib/astrails/safe/pgdump.rb +43 -0
- data/lib/astrails/safe/pipe.rb +15 -0
- data/lib/astrails/safe/s3.rb +68 -0
- data/lib/astrails/safe/sink.rb +33 -0
- data/lib/astrails/safe/source.rb +31 -0
- data/lib/astrails/safe/stream.rb +45 -0
- data/lib/astrails/safe/tmp_file.rb +25 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/templates/script.rb +122 -0
- metadata +94 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Stream do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@parent = Object.new
|
7
|
+
@stream = Astrails::Safe::Stream.new(@parent)
|
8
|
+
@r = rand(10)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.it_delegates_to_parent(prop)
|
12
|
+
it "delegates #{prop} to parent if not set" do
|
13
|
+
mock(@parent).__send__(prop) {@r}
|
14
|
+
@stream.send(prop).should == @r
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.it_delegates_to_parent_with_cache(prop)
|
19
|
+
it_delegates_to_parent(prop)
|
20
|
+
|
21
|
+
it "uses cached value for #{prop}" do
|
22
|
+
dont_allow(@parent).__send__(prop)
|
23
|
+
@stream.instance_variable_set "@#{prop}", @r + 1
|
24
|
+
@stream.send(prop).should == @r + 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it_delegates_to_parent_with_cache :id
|
29
|
+
it_delegates_to_parent_with_cache :config
|
30
|
+
|
31
|
+
it_delegates_to_parent :filename
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'extensions/mktmpdir'
|
2
|
+
require 'astrails/safe/tmp_file'
|
3
|
+
|
4
|
+
require 'astrails/safe/config/node'
|
5
|
+
require 'astrails/safe/config/builder'
|
6
|
+
|
7
|
+
require 'astrails/safe/stream'
|
8
|
+
|
9
|
+
require 'astrails/safe/source'
|
10
|
+
require 'astrails/safe/mysqldump'
|
11
|
+
require 'astrails/safe/pgdump'
|
12
|
+
require 'astrails/safe/archive'
|
13
|
+
|
14
|
+
require 'astrails/safe/pipe'
|
15
|
+
require 'astrails/safe/gpg'
|
16
|
+
require 'astrails/safe/gzip'
|
17
|
+
|
18
|
+
require 'astrails/safe/sink'
|
19
|
+
require 'astrails/safe/local'
|
20
|
+
require 'astrails/safe/s3'
|
21
|
+
|
22
|
+
|
23
|
+
module Astrails
|
24
|
+
module Safe
|
25
|
+
ROOT = File.join(File.dirname(__FILE__), "..", "..")
|
26
|
+
|
27
|
+
def timestamp
|
28
|
+
@timestamp ||= Time.now.strftime("%y%m%d-%H%M")
|
29
|
+
end
|
30
|
+
|
31
|
+
def safe(&block)
|
32
|
+
config = Config::Node.new(&block)
|
33
|
+
#config.dump
|
34
|
+
|
35
|
+
Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases])
|
36
|
+
Astrails::Safe::Pgdump.run(config[:pgdump, :databases])
|
37
|
+
Astrails::Safe::Archive.run(config[:tar, :archives])
|
38
|
+
|
39
|
+
Astrails::Safe::TmpFile.cleanup
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -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]] * " "
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
module Config
|
4
|
+
class Builder
|
5
|
+
COLLECTIONS = %w/database archive/
|
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/
|
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
|
+
die "#{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
|
+
die "#{sym}: unexpected: #{args.inspect}" unless args.empty?
|
36
|
+
die "#{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,67 @@
|
|
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
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gpg < Pipe
|
4
|
+
|
5
|
+
def compressed?
|
6
|
+
active? || @parent.compressed?
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
if key
|
13
|
+
rise RuntimeError, "can't use both gpg password and pubkey" if password
|
14
|
+
"|gpg -e -r #{key}"
|
15
|
+
elsif password
|
16
|
+
"|gpg -c --passphrase-file #{gpg_password_file(password)}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def extension
|
21
|
+
".gpg" if active?
|
22
|
+
end
|
23
|
+
|
24
|
+
def active?
|
25
|
+
password || key
|
26
|
+
end
|
27
|
+
|
28
|
+
def password
|
29
|
+
@password ||= config[:gpg, :password]
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
@key ||= config[:gpg, :key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def gpg_password_file(pass)
|
37
|
+
return "TEMP_GENERATED_FILENAME" if $DRY_RUN
|
38
|
+
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gzip < Pipe
|
4
|
+
|
5
|
+
def compressed?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
"|gzip" if active?
|
13
|
+
end
|
14
|
+
|
15
|
+
def extension
|
16
|
+
".gz" if active?
|
17
|
+
end
|
18
|
+
|
19
|
+
def active?
|
20
|
+
!@parent.compressed?
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Local < Sink
|
4
|
+
|
5
|
+
def open(&block)
|
6
|
+
return @parent.open(&block) unless active?
|
7
|
+
run
|
8
|
+
File.open(path, &block) unless $DRY_RUN
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def active?
|
14
|
+
# S3 can't upload from pipe. it needs to know file size, so we must pass through :local
|
15
|
+
# will change once we add SSH sink
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def prefix
|
20
|
+
@prefix ||= File.expand_path(expand(@config[:local, :path] || raise(RuntimeError, "missing :local/:path in configuration")))
|
21
|
+
end
|
22
|
+
|
23
|
+
def command
|
24
|
+
"#{@parent.command} > #{path}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
puts "command: #{command}" if $_VERBOSE
|
29
|
+
unless $DRY_RUN
|
30
|
+
FileUtils.mkdir_p(prefix) unless File.directory?(prefix)
|
31
|
+
system command
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def cleanup
|
36
|
+
return unless keep = @config[:keep, :local]
|
37
|
+
|
38
|
+
base = File.basename(filename).split(".").first
|
39
|
+
|
40
|
+
pattern = File.join(prefix, "#{base}*")
|
41
|
+
puts "listing files #{pattern.inspect}" if $_VERBOSE
|
42
|
+
files = Dir[pattern] .
|
43
|
+
select{|f| File.file?(f)} .
|
44
|
+
sort
|
45
|
+
|
46
|
+
cleanup_with_limit(files, keep) do |f|
|
47
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
48
|
+
File.unlink(f) unless $DRY_RUN
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mysqldump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
@commanbd ||= "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,43 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Pgdump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
@commanbd ||= "pg_dump #{postgres_options} #{postgres_username} #{postgres_password} #{postgres_host} #{postgres_port} #{@id}"
|
7
|
+
# @commanbd ||= "pg_dump -U #{@config["user"]} #{@config[:option]} #{@config["database"]} -f #{filename}"
|
8
|
+
# @commanbd ||= "mysqldump --defaults-extra-file=#{mysql_password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def extension; '.sql'; end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def postgres_options
|
16
|
+
@config[:options]
|
17
|
+
end
|
18
|
+
|
19
|
+
def postgres_host
|
20
|
+
@config["host"] ? "--host='#{@config["port"]}'" : ""
|
21
|
+
end
|
22
|
+
|
23
|
+
def postgres_port
|
24
|
+
@config["port"] ? "--port='#{@config["port"]}'" : ""
|
25
|
+
end
|
26
|
+
|
27
|
+
def postgres_username
|
28
|
+
@config["user"] ? "--username='#{@config["user"]}'" : ""
|
29
|
+
end
|
30
|
+
|
31
|
+
def postgres_password
|
32
|
+
`export PGPASSWORD=#{@config["password"]}` if @config["password"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def postgres_skip_tables
|
36
|
+
if skip_tables = @config[:skip_tables]
|
37
|
+
[*skip_tables].map { |t| "--exclude-table=#{@id}.#{t}" } * " "
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|