astrails-safe 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,7 +1,9 @@
1
1
  astrails-safe
2
2
  =============
3
3
 
4
- Simple mysql and filesystem backups with S3 support (with optional encryption)
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 databases and filesystem locally or to Amazon S3 (with optional encryption)"
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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 1
4
- :patch: 6
4
+ :patch: 7
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
@@ -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(File.dirname(__FILE__), '..', 'lib'))
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 "foo" do
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