astrails-safe 0.1.6 → 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.
- data/README.markdown +33 -3
- data/Rakefile +3 -3
- data/VERSION.yml +1 -1
- data/bin/astrails-safe +1 -7
- data/examples/example_helper.rb +3 -3
- data/examples/integration/archive_integration_example.rb +86 -0
- data/examples/unit/archive_example.rb +67 -0
- data/examples/unit/config_example.rb +49 -3
- data/examples/unit/gpg_example.rb +138 -0
- data/examples/unit/gzip_example.rb +64 -0
- data/examples/unit/local_example.rb +82 -0
- data/examples/unit/mysqldump_example.rb +83 -0
- data/examples/unit/pgdump_example.rb +45 -0
- data/examples/unit/s3_example.rb +28 -0
- data/examples/unit/svndump_example.rb +39 -0
- data/lib/astrails/safe.rb +24 -7
- data/lib/astrails/safe/archive.rb +2 -2
- data/lib/astrails/safe/backup.rb +20 -0
- data/lib/astrails/safe/config/builder.rb +6 -6
- data/lib/astrails/safe/config/node.rb +1 -2
- data/lib/astrails/safe/gpg.rb +14 -11
- data/lib/astrails/safe/gzip.rb +8 -8
- data/lib/astrails/safe/local.rb +8 -17
- data/lib/astrails/safe/mysqldump.rb +2 -2
- data/lib/astrails/safe/pgdump.rb +36 -0
- data/lib/astrails/safe/pipe.rb +5 -7
- data/lib/astrails/safe/s3.rb +9 -11
- data/lib/astrails/safe/sink.rb +7 -11
- data/lib/astrails/safe/source.rb +30 -15
- data/lib/astrails/safe/stream.rb +7 -33
- data/lib/astrails/safe/svndump.rb +13 -0
- data/lib/astrails/safe/tmp_file.rb +9 -5
- data/templates/script.rb +13 -0
- metadata +18 -5
- data/examples/unit/stream_example.rb +0 -33
data/README.markdown
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
astrails-safe
|
2
2
|
=============
|
3
3
|
|
4
|
-
Simple
|
4
|
+
Simple database and filesystem backups with S3 support (with optional encryption)
|
5
|
+
|
6
|
+
Home: github.com/astrails/safe
|
5
7
|
|
6
8
|
Motivation
|
7
9
|
----------
|
@@ -18,6 +20,13 @@ We needed a backup solution that will satisfy the following requirements:
|
|
18
20
|
|
19
21
|
And since we didn't find any, we wrote our own :)
|
20
22
|
|
23
|
+
Note
|
24
|
+
----
|
25
|
+
|
26
|
+
Support for pg_dump and svndump was contributed but since I don't personally use them i don't have an easy
|
27
|
+
way of testing it. So if you use Subversion or PostgreSQL please test the new functionality and report if
|
28
|
+
there are any problems.
|
29
|
+
|
21
30
|
Usage
|
22
31
|
-----
|
23
32
|
|
@@ -62,7 +71,7 @@ The procedure to create and transfer the key is as follows:
|
|
62
71
|
|
63
72
|
4. import public key on the remote system:
|
64
73
|
<pre>
|
65
|
-
$ gpg --import test@example.com.pub
|
74
|
+
$ gpg --import test@example.com.pub
|
66
75
|
gpg: key 45CA9403: public key "Test Backup <test@example.com>" imported
|
67
76
|
gpg: Total number processed: 1
|
68
77
|
gpg: imported: 1
|
@@ -83,7 +92,7 @@ The procedure to create and transfer the key is as follows:
|
|
83
92
|
4 = I trust fully
|
84
93
|
5 = I trust ultimately
|
85
94
|
m = back to the main menu
|
86
|
-
|
95
|
+
|
87
96
|
Your decision? 5
|
88
97
|
...
|
89
98
|
Command> quit
|
@@ -136,6 +145,22 @@ Example configuration
|
|
136
145
|
|
137
146
|
end
|
138
147
|
|
148
|
+
svndump do
|
149
|
+
repo :my_repo do
|
150
|
+
repo_path "/home/svn/my_repo"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
pgdump do
|
155
|
+
options "-i -x -O" # -i => ignore version, -x => do not dump privileges (grant/revoke), -O => skip restoration of object ownership in plain text format
|
156
|
+
|
157
|
+
user "username"
|
158
|
+
password "............" # shouldn't be used, instead setup ident. Current functionality exports a password env to the shell which pg_dump uses - untested!
|
159
|
+
|
160
|
+
database :blog
|
161
|
+
database :stateofflux_com
|
162
|
+
end
|
163
|
+
|
139
164
|
tar do
|
140
165
|
archive "git-repositories", :files => "/home/git/repositories"
|
141
166
|
archive "dot-configs", :files => "/home/*/.[^.]*"
|
@@ -154,6 +179,11 @@ Example configuration
|
|
154
179
|
end
|
155
180
|
</pre>
|
156
181
|
|
182
|
+
Reporting problems
|
183
|
+
------------------
|
184
|
+
|
185
|
+
http://github.com/astrails/safe/issues
|
186
|
+
|
157
187
|
Copyright
|
158
188
|
---------
|
159
189
|
|
data/Rakefile
CHANGED
@@ -5,11 +5,11 @@ begin
|
|
5
5
|
require 'jeweler'
|
6
6
|
Jeweler::Tasks.new do |gem|
|
7
7
|
gem.name = "safe"
|
8
|
-
gem.summary = %Q{Backup filesystem and MySQL to Amazon S3 (with encryption)}
|
9
|
-
gem.description = "Simple tool to backup MySQL
|
8
|
+
gem.summary = %Q{Backup filesystem and databases (MySQL and PostgreSQL) to Amazon S3 (with encryption)}
|
9
|
+
gem.description = "Simple tool to backup databases (MySQL and PostgreSQL) and filesystem locally or to Amazon S3 (with optional encryption)"
|
10
10
|
gem.email = "we@astrails.com"
|
11
11
|
gem.homepage = "http://github.com/astrails/safe"
|
12
|
-
gem.authors = ["Astrails Ltd."]
|
12
|
+
gem.authors = ["Astrails Ltd.", "Mark Mansour"]
|
13
13
|
gem.files = FileList["[A-Z]*.*", "{bin,examples,generators,lib,rails,spec,test,templates}/**/*", 'Rakefile', 'LICENSE*']
|
14
14
|
|
15
15
|
gem.add_dependency("aws-s3")
|
data/VERSION.yml
CHANGED
data/bin/astrails-safe
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require 'tempfile'
|
5
3
|
require 'rubygems'
|
6
|
-
require 'fileutils'
|
7
|
-
require "aws/s3"
|
8
|
-
require 'yaml'
|
9
4
|
|
10
5
|
#require 'ruby-debug'
|
11
6
|
#$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
@@ -52,8 +47,7 @@ def main
|
|
52
47
|
die "Created default #{$CONFIG_FILE_NAME}. Please edit and run again."
|
53
48
|
end
|
54
49
|
|
55
|
-
|
56
50
|
load($CONFIG_FILE_NAME)
|
57
51
|
end
|
58
52
|
|
59
|
-
main
|
53
|
+
main
|
data/examples/example_helper.rb
CHANGED
@@ -2,8 +2,9 @@ require 'rubygems'
|
|
2
2
|
require 'micronaut'
|
3
3
|
require 'ruby-debug'
|
4
4
|
|
5
|
+
SAFE_ROOT = File.dirname(File.dirname(__FILE__))
|
5
6
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
-
$LOAD_PATH.unshift(File.join(
|
7
|
+
$LOAD_PATH.unshift(File.join(SAFE_ROOT, 'lib'))
|
7
8
|
|
8
9
|
require 'astrails/safe'
|
9
10
|
|
@@ -15,5 +16,4 @@ Micronaut.configure do |c|
|
|
15
16
|
c.color_enabled = not_in_editor?
|
16
17
|
c.filter_run :focused => true
|
17
18
|
c.mock_with :rr
|
18
|
-
end
|
19
|
-
|
19
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
include FileUtils
|
5
|
+
|
6
|
+
describe "tar backup" do
|
7
|
+
before(:all) do
|
8
|
+
# need both local and instance vars
|
9
|
+
# instance variables are used in tests
|
10
|
+
# local variables are used in the backup definition (instance vars can't be seen)
|
11
|
+
@root = root = "tmp/archive_backup_example"
|
12
|
+
|
13
|
+
# clean state
|
14
|
+
rm_rf @root
|
15
|
+
mkdir_p @root
|
16
|
+
|
17
|
+
# create source tree
|
18
|
+
@src = src = "#{@root}/src"
|
19
|
+
mkdir_p "#{@src}/q/w/e"
|
20
|
+
mkdir_p "#{@src}/a/s/d"
|
21
|
+
|
22
|
+
File.open("#{@src}/qwe1", "w") {|f| f.write("qwe") }
|
23
|
+
File.open("#{@src}/q/qwe2", "w") {|f| f.write("qwe"*2) }
|
24
|
+
File.open("#{@src}/q/w/qwe3", "w") {|f| f.write("qwe"*3) }
|
25
|
+
File.open("#{@src}/q/w/e/qwe4", "w") {|f| f.write("qwe"*4) }
|
26
|
+
|
27
|
+
File.open("#{@src}/asd1", "w") {|f| f.write("asd") }
|
28
|
+
File.open("#{@src}/a/asd2", "w") {|f| f.write("asd" * 2) }
|
29
|
+
File.open("#{@src}/a/s/asd3", "w") {|f| f.write("asd" * 3) }
|
30
|
+
|
31
|
+
@dst = dst = "#{@root}/backup"
|
32
|
+
mkdir_p @dst
|
33
|
+
|
34
|
+
@now = Time.now
|
35
|
+
@timestamp = @now.strftime("%y%m%d-%H%M")
|
36
|
+
|
37
|
+
stub(Time).now {@now} # Freeze
|
38
|
+
|
39
|
+
Astrails::Safe.safe do
|
40
|
+
local :path => "#{dst}/:kind"
|
41
|
+
tar do
|
42
|
+
archive :test1 do
|
43
|
+
files src
|
44
|
+
exclude "#{src}/q/w"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
@backup = "#{dst}/archive/archive-test1.#{@timestamp}.tar.gz"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should create backup file" do
|
53
|
+
puts "Expecting: #{@backup}"
|
54
|
+
File.exists?(@backup).should be_true
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "after extracting" do
|
58
|
+
before(:all) do
|
59
|
+
# prepare target dir
|
60
|
+
@target = "#{@root}/test"
|
61
|
+
mkdir_p @target
|
62
|
+
system "tar -zxvf #{@backup} -C #{@target}"
|
63
|
+
|
64
|
+
@test = "#{@target}/#{@root}/src"
|
65
|
+
puts @test
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should include asd1/2/3" do
|
69
|
+
File.exists?("#{@test}/asd1").should be_true
|
70
|
+
File.exists?("#{@test}/a/asd2").should be_true
|
71
|
+
File.exists?("#{@test}/a/s/asd3").should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should only include qwe 1 and 2 (no 3)" do
|
75
|
+
File.exists?("#{@test}/qwe1").should be_true
|
76
|
+
File.exists?("#{@test}/q/qwe2").should be_true
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should preserve file content" do
|
80
|
+
File.read("#{@test}/qwe1").should == "qwe"
|
81
|
+
File.read("#{@test}/q/qwe2").should == "qweqwe"
|
82
|
+
File.read("#{@test}/a/s/asd3").should == "asdasdasd"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Archive do
|
4
|
+
|
5
|
+
def def_config
|
6
|
+
{
|
7
|
+
:options => "OPTS",
|
8
|
+
:files => "apples",
|
9
|
+
:exclude => "oranges"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def archive(id = :foo, config = def_config)
|
14
|
+
Astrails::Safe::Archive.new(id, Astrails::Safe::Config::Node.new(nil, config))
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:each) { Astrails::Safe::TmpFile.cleanup }
|
18
|
+
|
19
|
+
describe :backup do
|
20
|
+
before(:each) do
|
21
|
+
@archive = archive
|
22
|
+
stub(@archive).timestamp {"NOW"}
|
23
|
+
end
|
24
|
+
|
25
|
+
{
|
26
|
+
:id => "foo",
|
27
|
+
:kind => "archive",
|
28
|
+
:extension => ".tar",
|
29
|
+
:filename => "archive-foo.NOW",
|
30
|
+
:command => "tar -cf - OPTS --exclude=oranges apples",
|
31
|
+
}.each do |k, v|
|
32
|
+
it "should set #{k} to #{v}" do
|
33
|
+
@archive.backup.send(k).should == v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe :tar_exclude_files do
|
39
|
+
it "should return '' when no excludes" do
|
40
|
+
archive(:foo, {}).send(:tar_exclude_files).should == ''
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should accept single exclude as string" do
|
44
|
+
archive(:foo, {:exclude => "bar"}).send(:tar_exclude_files).should == '--exclude=bar'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should accept multiple exclude as array" do
|
48
|
+
archive(:foo, {:exclude => ["foo", "bar"]}).send(:tar_exclude_files).should == '--exclude=foo --exclude=bar'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe :tar_files do
|
53
|
+
it "should raise RuntimeError when no files" do
|
54
|
+
lambda {
|
55
|
+
archive(:foo, {}).send(:tar_files)
|
56
|
+
}.should raise_error(RuntimeError, "missing files for tar")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should accept single file as string" do
|
60
|
+
archive(:foo, {:files => "foo"}).send(:tar_files).should == "foo"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should accept multiple files as array" do
|
64
|
+
archive(:foo, {:files => ["foo", "bar"]}).send(:tar_files).should == "foo bar"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
2
|
|
3
3
|
describe Astrails::Safe::Config do
|
4
|
-
it "
|
4
|
+
it "should parse example config" do
|
5
5
|
config = Astrails::Safe::Config::Node.new do
|
6
6
|
local do
|
7
7
|
path "path"
|
@@ -47,6 +47,29 @@ describe Astrails::Safe::Config do
|
|
47
47
|
|
48
48
|
end
|
49
49
|
|
50
|
+
pgdump do
|
51
|
+
options "-i -x -O"
|
52
|
+
|
53
|
+
user "astrails"
|
54
|
+
password ""
|
55
|
+
host "localhost"
|
56
|
+
port 5432
|
57
|
+
|
58
|
+
database :blog
|
59
|
+
|
60
|
+
database :production do
|
61
|
+
keep :local => 3
|
62
|
+
|
63
|
+
skip_tables [:logger_exceptions, :request_logs]
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
svndump do
|
69
|
+
repo :my_repo do
|
70
|
+
repo_path "/home/svn/my_repo"
|
71
|
+
end
|
72
|
+
end
|
50
73
|
|
51
74
|
tar do
|
52
75
|
archive "git-repositories" do
|
@@ -105,6 +128,31 @@ describe Astrails::Safe::Config do
|
|
105
128
|
},
|
106
129
|
},
|
107
130
|
},
|
131
|
+
|
132
|
+
"pgdump" => {
|
133
|
+
"options" => "-i -x -O",
|
134
|
+
"user" => "astrails",
|
135
|
+
"password" => "",
|
136
|
+
"host" => "localhost",
|
137
|
+
"port" => 5432,
|
138
|
+
|
139
|
+
"databases" => {
|
140
|
+
"blog" => {},
|
141
|
+
"production" => {
|
142
|
+
"keep" => {"local" => 3},
|
143
|
+
"skip_tables" => ["logger_exceptions", "request_logs"],
|
144
|
+
},
|
145
|
+
},
|
146
|
+
},
|
147
|
+
|
148
|
+
"svndump" => {
|
149
|
+
"repos" => {
|
150
|
+
"my_repo"=> {
|
151
|
+
"repo_path" => "/home/svn/my_repo"
|
152
|
+
}
|
153
|
+
}
|
154
|
+
},
|
155
|
+
|
108
156
|
"tar" => {
|
109
157
|
"archives" => {
|
110
158
|
"git-repositories" => {"files" => "/home/git/repositories"},
|
@@ -120,7 +168,5 @@ describe Astrails::Safe::Config do
|
|
120
168
|
}
|
121
169
|
|
122
170
|
config.to_hash.should == expected
|
123
|
-
|
124
171
|
end
|
125
172
|
end
|
126
|
-
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Gpg do
|
4
|
+
def def_backup
|
5
|
+
{
|
6
|
+
:compressed => false,
|
7
|
+
:command => "command",
|
8
|
+
:extension => ".foo",
|
9
|
+
:filename => "qweqwe"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def gpg(config = {}, backup = def_backup)
|
14
|
+
Astrails::Safe::Gpg.new(
|
15
|
+
@config = Astrails::Safe::Config::Node.new(nil, config),
|
16
|
+
@backup = Astrails::Safe::Backup.new(backup)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
before(:each) do
|
21
|
+
@gpg = gpg()
|
22
|
+
stub(@gpg).gpg_password_file {"pwd-file"}
|
23
|
+
stub(@gpg).pipe {"|gpg -BLAH"}
|
24
|
+
end
|
25
|
+
|
26
|
+
after(:each) { Astrails::Safe::TmpFile.cleanup }
|
27
|
+
|
28
|
+
describe :process do
|
29
|
+
|
30
|
+
describe "when active" do
|
31
|
+
before(:each) do
|
32
|
+
stub(@gpg).active? {true}
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should add .gpg extension" do
|
36
|
+
mock(@backup.extension) << '.gpg'
|
37
|
+
@gpg.process
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should add command pipe" do
|
41
|
+
mock(@backup.command) << (/\|gpg -/)
|
42
|
+
@gpg.process
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set compressed" do
|
46
|
+
mock(@backup).compressed = true
|
47
|
+
@gpg.process
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "when inactive" do
|
52
|
+
before(:each) do
|
53
|
+
stub(@gpg).active? {false}
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should not touch extension" do
|
57
|
+
dont_allow(@backup.extension) << anything
|
58
|
+
@gpg.process
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not touch command" do
|
62
|
+
dont_allow(@backup.command) << anything
|
63
|
+
@gpg.process
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should not touch compressed" do
|
67
|
+
dont_allow(@backup).compressed = anything
|
68
|
+
@gpg.process
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe :active? do
|
74
|
+
|
75
|
+
describe "with key" do
|
76
|
+
it "should be true" do
|
77
|
+
gpg(:gpg => {:key => :foo}).should be_active
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "with password" do
|
82
|
+
it "should be true" do
|
83
|
+
gpg(:gpg => {:password => :foo}).should be_active
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "without key & password" do
|
88
|
+
it "should be false" do
|
89
|
+
gpg.should_not be_active
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "with key & password" do
|
94
|
+
it "should raise RuntimeError" do
|
95
|
+
lambda {
|
96
|
+
gpg(:gpg => {:key => "foo", :password => "bar"}).send :active?
|
97
|
+
}.should raise_error(RuntimeError, "can't use both gpg password and pubkey")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe :pipe do
|
103
|
+
|
104
|
+
describe "with key" do
|
105
|
+
before(:each) do
|
106
|
+
@gpg = gpg(:gpg => {:key => "foo"}, :options => "OPT")
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should not call gpg_password_file" do
|
110
|
+
dont_allow(@gpg).gpg_password_file(anything)
|
111
|
+
@gpg.send(:pipe)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should use '-r' and options" do
|
115
|
+
@gpg.send(:pipe).should == "|gpg OPT -e -r foo"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "with password" do
|
120
|
+
before(:each) do
|
121
|
+
@gpg = gpg(:gpg => {:password => "bar"}, :options => "OPT")
|
122
|
+
stub(@gpg).gpg_password_file(anything) {"pass-file"}
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should use '--passphrase-file' and :options" do
|
126
|
+
@gpg.send(:pipe).should == "|gpg OPT -c --passphrase-file pass-file"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe :gpg_password_file do
|
132
|
+
it "should create password file" do
|
133
|
+
file = gpg.send(:gpg_password_file, "foo")
|
134
|
+
File.exists?(file).should be_true
|
135
|
+
File.read(file).should == "foo"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|