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 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