markmansour-safe 0.1.7

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.
@@ -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
@@ -0,0 +1,15 @@
1
+ module Astrails
2
+ module Safe
3
+ class Pipe < Stream
4
+
5
+ def command
6
+ "#{@parent.command}#{pipe}"
7
+ end
8
+
9
+ def filename
10
+ "#{@parent.filename}#{extension}"
11
+ end
12
+
13
+ end
14
+ end
15
+ end