netguru-safe 0.2.9

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.
Files changed (54) hide show
  1. data/.autotest +42 -0
  2. data/.document +5 -0
  3. data/.gitignore +11 -0
  4. data/CHANGELOG +25 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +44 -0
  7. data/LICENSE +20 -0
  8. data/README.markdown +237 -0
  9. data/Rakefile +32 -0
  10. data/TODO +11 -0
  11. data/VERSION +1 -0
  12. data/astrails-safe.gemspec +37 -0
  13. data/bin/astrails-safe +53 -0
  14. data/examples/example_helper.rb +19 -0
  15. data/lib/astrails/safe.rb +73 -0
  16. data/lib/astrails/safe/archive.rb +24 -0
  17. data/lib/astrails/safe/backup.rb +20 -0
  18. data/lib/astrails/safe/cloudfiles.rb +77 -0
  19. data/lib/astrails/safe/config/builder.rb +60 -0
  20. data/lib/astrails/safe/config/node.rb +76 -0
  21. data/lib/astrails/safe/gpg.rb +46 -0
  22. data/lib/astrails/safe/gzip.rb +25 -0
  23. data/lib/astrails/safe/local.rb +51 -0
  24. data/lib/astrails/safe/mongodump.rb +23 -0
  25. data/lib/astrails/safe/mysqldump.rb +32 -0
  26. data/lib/astrails/safe/pgdump.rb +36 -0
  27. data/lib/astrails/safe/pipe.rb +17 -0
  28. data/lib/astrails/safe/s3.rb +75 -0
  29. data/lib/astrails/safe/sftp.rb +88 -0
  30. data/lib/astrails/safe/sink.rb +35 -0
  31. data/lib/astrails/safe/source.rb +47 -0
  32. data/lib/astrails/safe/stream.rb +20 -0
  33. data/lib/astrails/safe/svndump.rb +13 -0
  34. data/lib/astrails/safe/tmp_file.rb +48 -0
  35. data/lib/astrails/safe/version.rb +5 -0
  36. data/lib/extensions/mktmpdir.rb +45 -0
  37. data/spec/integration/airbrake_integration_spec.rb +76 -0
  38. data/spec/integration/archive_integration_spec.rb +88 -0
  39. data/spec/integration/cleanup_spec.rb +61 -0
  40. data/spec/spec.opts +5 -0
  41. data/spec/spec_helper.rb +13 -0
  42. data/spec/unit/archive_spec.rb +67 -0
  43. data/spec/unit/cloudfiles_spec.rb +177 -0
  44. data/spec/unit/config_spec.rb +234 -0
  45. data/spec/unit/gpg_spec.rb +148 -0
  46. data/spec/unit/gzip_spec.rb +64 -0
  47. data/spec/unit/local_spec.rb +110 -0
  48. data/spec/unit/mongodump_spec.rb +54 -0
  49. data/spec/unit/mysqldump_spec.rb +83 -0
  50. data/spec/unit/pgdump_spec.rb +45 -0
  51. data/spec/unit/s3_spec.rb +163 -0
  52. data/spec/unit/svndump_spec.rb +39 -0
  53. data/templates/script.rb +160 -0
  54. metadata +222 -0
@@ -0,0 +1,177 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Astrails::Safe::Cloudfiles do
4
+
5
+ def def_config
6
+ {
7
+ :cloudfiles => {
8
+ :container => "_container",
9
+ :user => "_user",
10
+ :api_key => "_api_key",
11
+ },
12
+ :keep => {
13
+ :cloudfiles => 2
14
+ }
15
+ }
16
+ end
17
+
18
+ def def_backup(extra = {})
19
+ {
20
+ :kind => "_kind",
21
+ :filename => "/backup/somewhere/_kind-_id.NOW.bar",
22
+ :extension => ".bar",
23
+ :id => "_id",
24
+ :timestamp => "NOW"
25
+ }.merge(extra)
26
+ end
27
+
28
+ def cloudfiles(config = def_config, backup = def_backup)
29
+ Astrails::Safe::Cloudfiles.new(
30
+ Astrails::Safe::Config::Node.new(nil, config),
31
+ Astrails::Safe::Backup.new(backup)
32
+ )
33
+ end
34
+
35
+ describe :cleanup do
36
+
37
+ before(:each) do
38
+ @cloudfiles = cloudfiles
39
+
40
+ @files = [4,1,3,2].map { |i| "aaaaa#{i}" }
41
+
42
+ @container = "container"
43
+
44
+ stub(@container).objects(:prefix => "_kind/_id/_kind-_id.") { @files }
45
+ stub(@container).delete_object(anything)
46
+
47
+ stub(CloudFiles::Connection).
48
+ new('_user', '_api_key', true, false).stub!.
49
+ container('_container') {@container}
50
+ end
51
+
52
+ it "should check [:keep, :cloudfiles]" do
53
+ @cloudfiles.config[:keep].data["cloudfiles"] = nil
54
+ dont_allow(@cloudfiles.backup).filename
55
+ @cloudfiles.send :cleanup
56
+ end
57
+
58
+ it "should delete extra files" do
59
+ mock(@container).delete_object('aaaaa1')
60
+ mock(@container).delete_object('aaaaa2')
61
+ @cloudfiles.send :cleanup
62
+ end
63
+
64
+ end
65
+
66
+ describe :active do
67
+ before(:each) do
68
+ @cloudfiles = cloudfiles
69
+ end
70
+
71
+ it "should be true when all params are set" do
72
+ @cloudfiles.should be_active
73
+ end
74
+
75
+ it "should be false if container is missing" do
76
+ @cloudfiles.config[:cloudfiles].data["container"] = nil
77
+ @cloudfiles.should_not be_active
78
+ end
79
+
80
+ it "should be false if user is missing" do
81
+ @cloudfiles.config[:cloudfiles].data["user"] = nil
82
+ @cloudfiles.should_not be_active
83
+ end
84
+
85
+ it "should be false if api_key is missing" do
86
+ @cloudfiles.config[:cloudfiles].data["api_key"] = nil
87
+ @cloudfiles.should_not be_active
88
+ end
89
+ end
90
+
91
+ describe :path do
92
+ before(:each) do
93
+ @cloudfiles = cloudfiles
94
+ end
95
+ it "should use cloudfiles/path 1st" do
96
+ @cloudfiles.config[:cloudfiles].data["path"] = "cloudfiles_path"
97
+ @cloudfiles.config[:local] = {:path => "local_path"}
98
+ @cloudfiles.send(:path).should == "cloudfiles_path"
99
+ end
100
+
101
+ it "should use local/path 2nd" do
102
+ @cloudfiles.config[:local] = {:path => "local_path"}
103
+ @cloudfiles.send(:path).should == "local_path"
104
+ end
105
+
106
+ it "should use constant 3rd" do
107
+ @cloudfiles.send(:path).should == "_kind/_id"
108
+ end
109
+
110
+ end
111
+
112
+ describe :save do
113
+ def add_stubs(*stubs)
114
+ stubs.each do |s|
115
+ case s
116
+ when :connection
117
+ @connection = "connection"
118
+ stub(CloudFiles::Authentication).new
119
+ stub(CloudFiles::Connection).
120
+ new('_user', '_api_key', true, false) {@connection}
121
+ when :file_size
122
+ stub(@cloudfiles).get_file_size("foo") {123}
123
+ when :create_container
124
+ @container = "container"
125
+ stub(@container).create_object("_kind/_id/backup/somewhere/_kind-_id.NOW.bar.bar", true) {@object}
126
+ stub(@connection).create_container {@container}
127
+ when :file_open
128
+ stub(File).open("foo")
129
+ when :cloudfiles_store
130
+ @object = "object"
131
+ stub(@object).write(nil) {true}
132
+ end
133
+ end
134
+ end
135
+
136
+ before(:each) do
137
+ @cloudfiles = cloudfiles(def_config, def_backup(:path => "foo"))
138
+ @full_path = "_kind/_id/backup/somewhere/_kind-_id.NOW.bar.bar"
139
+ end
140
+
141
+ it "should fail if no backup.file is set" do
142
+ @cloudfiles.backup.path = nil
143
+ proc {@cloudfiles.send(:save)}.should raise_error(RuntimeError)
144
+ end
145
+
146
+ it "should establish Cloud Files connection" do
147
+ add_stubs(:connection, :file_size, :create_container, :file_open, :cloudfiles_store)
148
+ @cloudfiles.send(:save)
149
+ end
150
+
151
+ it "should open local file" do
152
+ add_stubs(:connection, :file_size, :create_container, :cloudfiles_store)
153
+ mock(File).open("foo")
154
+ @cloudfiles.send(:save)
155
+ end
156
+
157
+ it "should call write on the cloudfile object with files' descriptor" do
158
+ add_stubs(:connection, :file_size, :create_container, :cloudfiles_store)
159
+ stub(File).open("foo") {"qqq"}
160
+ mock(@object).write("qqq") {true}
161
+ @cloudfiles.send(:save)
162
+ end
163
+
164
+ it "should upload file" do
165
+ add_stubs(:connection, :file_size, :create_container, :file_open, :cloudfiles_store)
166
+ @cloudfiles.send(:save)
167
+ end
168
+
169
+ it "should fail on files bigger then 5G" do
170
+ add_stubs(:connection)
171
+ mock(File).stat("foo").stub!.size {5*1024*1024*1024+1}
172
+ mock(STDERR).puts(anything)
173
+ dont_allow(Benchmark).realtime
174
+ @cloudfiles.send(:save)
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,234 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Astrails::Safe::Config do
4
+ it "should parse example config" do
5
+ config = Astrails::Safe::Config::Node.new do
6
+ airbrake do
7
+ api_key "test_airbrake_api_key"
8
+ end
9
+
10
+ local do
11
+ path "path"
12
+ end
13
+
14
+ s3 do
15
+ key "s3 key"
16
+ secret "secret"
17
+ bucket "bucket"
18
+ path "path1"
19
+ end
20
+
21
+ sftp do
22
+ user "sftp user"
23
+ password "sftp password"
24
+ host "sftp host"
25
+ end
26
+
27
+ gpg do
28
+ key "gpg-key"
29
+ password "astrails"
30
+ end
31
+
32
+ keep do
33
+ local 4
34
+ s3 20
35
+ end
36
+
37
+ mysqldump do
38
+ options "-ceKq --single-transaction --create-options"
39
+
40
+ user "astrails"
41
+ password ""
42
+ host "localhost"
43
+ port 3306
44
+ socket "/var/run/mysqld/mysqld.sock"
45
+
46
+ database :blog
47
+
48
+ database :production do
49
+ keep :local => 3
50
+
51
+ gpg do
52
+ password "custom-production-pass"
53
+ end
54
+
55
+ skip_tables [:logger_exceptions, :request_logs]
56
+ end
57
+
58
+ end
59
+
60
+ pgdump do
61
+ options "-i -x -O"
62
+
63
+ user "astrails"
64
+ password ""
65
+ host "localhost"
66
+ port 5432
67
+
68
+ database :blog
69
+
70
+ database :production do
71
+ keep :local => 3
72
+
73
+ skip_tables [:logger_exceptions, :request_logs]
74
+ end
75
+
76
+ end
77
+
78
+ svndump do
79
+ repo :my_repo do
80
+ repo_path "/home/svn/my_repo"
81
+ end
82
+ end
83
+
84
+ tar do
85
+ archive "git-repositories" do
86
+ files "/home/git/repositories"
87
+ end
88
+
89
+ archive "etc-files" do
90
+ files "/etc"
91
+ exclude "/etc/puppet/other"
92
+ end
93
+
94
+ archive "dot-configs" do
95
+ files "/home/*/.[^.]*"
96
+ end
97
+
98
+ archive "blog" do
99
+ files "/var/www/blog.astrails.com/"
100
+ exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
101
+ end
102
+
103
+ archive :misc do
104
+ files [ "/backup/*.rb" ]
105
+ end
106
+ end
107
+
108
+ mongodump do
109
+ host "host"
110
+ database "database"
111
+ user "user"
112
+ password "password"
113
+ end
114
+ end
115
+
116
+ expected = {
117
+ "airbrake" => {"api_key" => "test_airbrake_api_key"},
118
+
119
+ "local" => {"path" => "path"},
120
+
121
+ "s3" => {
122
+ "key" => "s3 key",
123
+ "secret" => "secret",
124
+ "bucket" => "bucket",
125
+ "path" => "path1",
126
+ },
127
+
128
+ "sftp" => {
129
+ "user" => "sftp user",
130
+ "password" => "sftp password",
131
+ "host" => "sftp host",
132
+ },
133
+
134
+ "gpg" => {"password" => "astrails", "key" => "gpg-key"},
135
+
136
+ "keep" => {"s3" => 20, "local" => 4},
137
+
138
+ "mysqldump" => {
139
+ "options" => "-ceKq --single-transaction --create-options",
140
+ "user" => "astrails",
141
+ "password" => "",
142
+ "host" => "localhost",
143
+ "port" => 3306,
144
+ "socket" => "/var/run/mysqld/mysqld.sock",
145
+
146
+ "databases" => {
147
+ "blog" => {},
148
+ "production" => {
149
+ "keep" => {"local" => 3},
150
+ "gpg" => {"password" => "custom-production-pass"},
151
+ "skip_tables" => ["logger_exceptions", "request_logs"],
152
+ },
153
+ },
154
+ },
155
+
156
+ "pgdump" => {
157
+ "options" => "-i -x -O",
158
+ "user" => "astrails",
159
+ "password" => "",
160
+ "host" => "localhost",
161
+ "port" => 5432,
162
+
163
+ "databases" => {
164
+ "blog" => {},
165
+ "production" => {
166
+ "keep" => {"local" => 3},
167
+ "skip_tables" => ["logger_exceptions", "request_logs"],
168
+ },
169
+ },
170
+ },
171
+
172
+ "svndump" => {
173
+ "repos" => {
174
+ "my_repo"=> {
175
+ "repo_path" => "/home/svn/my_repo"
176
+ }
177
+ }
178
+ },
179
+
180
+ "tar" => {
181
+ "archives" => {
182
+ "git-repositories" => {"files" => "/home/git/repositories"},
183
+ "etc-files" => {"files" => "/etc", "exclude" => "/etc/puppet/other"},
184
+ "dot-configs" => {"files" => "/home/*/.[^.]*"},
185
+ "blog" => {
186
+ "files" => "/var/www/blog.astrails.com/",
187
+ "exclude" => ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"],
188
+ },
189
+ "misc" => { "files" => ["/backup/*.rb"] },
190
+ },
191
+ },
192
+
193
+ "mongodump" => {
194
+ "host" => "host",
195
+ "databases" => {
196
+ "database" => {}
197
+ },
198
+ "user" => "user",
199
+ "password" => "password"
200
+ }
201
+ }
202
+
203
+ config.to_hash.should == expected
204
+ end
205
+
206
+ it "should make an array from multivalues" do
207
+ config = Astrails::Safe::Config::Node.new do
208
+ skip_tables "a"
209
+ skip_tables "b"
210
+ files "/foo"
211
+ files "/bar"
212
+ exclude "/foo/bar"
213
+ exclude "/foo/bar/baz"
214
+ end
215
+
216
+ expected = {
217
+ "skip_tables" => ["a", "b"],
218
+ "files" => ["/foo", "/bar"],
219
+ "exclude" => ["/foo/bar", "/foo/bar/baz"],
220
+ }
221
+
222
+ config.to_hash.should == expected
223
+ end
224
+
225
+ it "should raise error on key duplication" do
226
+ proc do
227
+ Astrails::Safe::Config::Node.new do
228
+ path "foo"
229
+ path "bar"
230
+ end
231
+ end.should raise_error(ArgumentError, "duplicate value for 'path'")
232
+ end
233
+
234
+ end
@@ -0,0 +1,148 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_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
+ Astrails::Safe::Config::Node.new(nil, config),
16
+ Astrails::Safe::Backup.new(backup)
17
+ )
18
+ end
19
+
20
+ after(:each) { Astrails::Safe::TmpFile.cleanup }
21
+
22
+ describe :process do
23
+
24
+ before(:each) do
25
+ @gpg = gpg()
26
+ stub(@gpg).gpg_password_file {"pwd-file"}
27
+ stub(@gpg).pipe {"|gpg -BLAH"}
28
+ end
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(@gpg.backup.extension) << '.gpg'
37
+ @gpg.process
38
+ end
39
+
40
+ it "should add command pipe" do
41
+ mock(@gpg.backup.command) << (/\|gpg -BLAH/)
42
+ @gpg.process
43
+ end
44
+
45
+ it "should set compressed" do
46
+ mock(@gpg.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(@gpg.backup.extension) << anything
58
+ @gpg.process
59
+ end
60
+
61
+ it "should not touch command" do
62
+ dont_allow(@gpg.backup.command) << anything
63
+ @gpg.process
64
+ end
65
+
66
+ it "should not touch compressed" do
67
+ dont_allow(@gpg.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
+ def kgpg(extra={})
106
+ gpg({:gpg => {:key => "foo", :options => "GPG-OPT"}.merge(extra), :options => "OPT"})
107
+ end
108
+
109
+ it "should not call gpg_password_file" do
110
+ g = kgpg
111
+ dont_allow(g).gpg_password_file(anything)
112
+ g.send(:pipe)
113
+ end
114
+
115
+ it "should use '-r' and :options" do
116
+ kgpg.send(:pipe).should == "|gpg GPG-OPT -e -r foo"
117
+ end
118
+
119
+ it "should use the 'command' options" do
120
+ kgpg(:command => 'other-gpg').send(:pipe).should == "|other-gpg GPG-OPT -e -r foo"
121
+ end
122
+ end
123
+
124
+ describe "with password" do
125
+ def pgpg(extra = {})
126
+ returning(gpg({:gpg => {:password => "bar", :options => "GPG-OPT"}.merge(extra), :options => "OPT"})) do |g|
127
+ stub(g).gpg_password_file(anything) {"pass-file"}
128
+ end
129
+ end
130
+
131
+ it "should use '--passphrase-file' and :options" do
132
+ pgpg.send(:pipe).should == "|gpg GPG-OPT -c --passphrase-file pass-file"
133
+ end
134
+
135
+ it "should use the 'command' options" do
136
+ pgpg(:command => 'other-gpg').send(:pipe).should == "|other-gpg GPG-OPT -c --passphrase-file pass-file"
137
+ end
138
+ end
139
+ end
140
+
141
+ describe :gpg_password_file do
142
+ it "should create password file" do
143
+ file = gpg.send(:gpg_password_file, "foo")
144
+ File.exists?(file).should be_true
145
+ File.read(file).should == "foo"
146
+ end
147
+ end
148
+ end