darkofabijan-astrails-safe 0.2.8

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 (41) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +237 -0
  3. data/Rakefile +61 -0
  4. data/bin/astrails-safe +53 -0
  5. data/examples/example_helper.rb +19 -0
  6. data/lib/astrails/safe.rb +61 -0
  7. data/lib/astrails/safe/archive.rb +24 -0
  8. data/lib/astrails/safe/backup.rb +20 -0
  9. data/lib/astrails/safe/cloudfiles.rb +70 -0
  10. data/lib/astrails/safe/config/builder.rb +60 -0
  11. data/lib/astrails/safe/config/node.rb +76 -0
  12. data/lib/astrails/safe/gpg.rb +46 -0
  13. data/lib/astrails/safe/gzip.rb +25 -0
  14. data/lib/astrails/safe/local.rb +70 -0
  15. data/lib/astrails/safe/mysqldump.rb +32 -0
  16. data/lib/astrails/safe/pgdump.rb +36 -0
  17. data/lib/astrails/safe/pipe.rb +17 -0
  18. data/lib/astrails/safe/s3.rb +86 -0
  19. data/lib/astrails/safe/sftp.rb +88 -0
  20. data/lib/astrails/safe/sink.rb +35 -0
  21. data/lib/astrails/safe/source.rb +47 -0
  22. data/lib/astrails/safe/stream.rb +20 -0
  23. data/lib/astrails/safe/svndump.rb +13 -0
  24. data/lib/astrails/safe/tmp_file.rb +48 -0
  25. data/lib/extensions/mktmpdir.rb +45 -0
  26. data/spec/integration/archive_integration_spec.rb +88 -0
  27. data/spec/integration/cleanup_spec.rb +61 -0
  28. data/spec/spec.opts +5 -0
  29. data/spec/spec_helper.rb +16 -0
  30. data/spec/unit/archive_spec.rb +67 -0
  31. data/spec/unit/cloudfiles_spec.rb +170 -0
  32. data/spec/unit/config_spec.rb +213 -0
  33. data/spec/unit/gpg_spec.rb +148 -0
  34. data/spec/unit/gzip_spec.rb +64 -0
  35. data/spec/unit/local_spec.rb +110 -0
  36. data/spec/unit/mysqldump_spec.rb +83 -0
  37. data/spec/unit/pgdump_spec.rb +45 -0
  38. data/spec/unit/s3_spec.rb +160 -0
  39. data/spec/unit/svndump_spec.rb +39 -0
  40. data/templates/script.rb +165 -0
  41. metadata +179 -0
@@ -0,0 +1,61 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_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/cleanup_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
20
+
21
+ File.open(qwe = "#{@src}/qwe", "w") {|f| f.write("qwe") }
22
+
23
+ @dst = dst = "#{@root}/backup"
24
+ mkdir_p "#{@dst}/archive"
25
+
26
+ @now = Time.now
27
+ @timestamp = @now.strftime("%y%m%d-%H%M")
28
+
29
+ stub(Time).now {@now} # Freeze
30
+
31
+ cp qwe, "#{dst}/archive/archive-foo.000001.tar.gz"
32
+ cp qwe, "#{dst}/archive/archive-foo.000002.tar.gz"
33
+ cp qwe, "#{dst}/archive/archive-foobar.000001.tar.gz"
34
+ cp qwe, "#{dst}/archive/archive-foobar.000002.tar.gz"
35
+
36
+ Astrails::Safe.safe do
37
+ local :path => "#{dst}/:kind"
38
+ tar do
39
+ keep :local => 1 # only leave the latest
40
+ archive :foo do
41
+ files src
42
+ end
43
+ end
44
+ end
45
+
46
+ @backup = "#{dst}/archive/archive-foo.#{@timestamp}.tar.gz"
47
+ end
48
+
49
+ it "should create backup file" do
50
+ File.exists?(@backup).should be_true
51
+ end
52
+
53
+ it "should remove old backups" do
54
+ Dir["#{@dst}/archive/archive-foo.*"].should == [@backup]
55
+ end
56
+
57
+ it "should NOT remove backups with base having same prefix" do
58
+ Dir["#{@dst}/archive/archive-foobar.*"].should == ["#{@dst}/archive/archive-foobar.000001.tar.gz", "#{@dst}/archive/archive-foobar.000002.tar.gz"]
59
+ end
60
+
61
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,5 @@
1
+ --colour
2
+ --format specdoc
3
+ --format profile:log/spec-benchmark.log
4
+ --loadby mtime
5
+ --reverse
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'spec'
4
+ require 'spec/autorun'
5
+
6
+ require 'rubygems'
7
+ require 'ruby-debug'
8
+ require 'yaml'
9
+
10
+ require 'astrails/safe'
11
+
12
+ Spec::Runner.configure do |config|
13
+ config.mock_with :rr
14
+ end
15
+
16
+ SERVICES_CONFIG = File.open( 'spec/integration_config.yml' ) { |yf| YAML::load( yf ) }
@@ -0,0 +1,67 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_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
@@ -0,0 +1,170 @@
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].to_a.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
+ stub(CloudFiles::Authentication).new
118
+ stub(CloudFiles::Connection).
119
+ new('_user', '_api_key', true, false).stub!.
120
+ create_container('_container') {@container}
121
+ when :stat
122
+ stub(File).stat("foo").stub!.size {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(CloudFiles::Connection).create_container {@container}
127
+ when :file_open
128
+ stub(File).open("foo")
129
+ when :cloudfiles_store
130
+ @object = "object"
131
+ mock(@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, :stat, :create_container, :file_open, :cloudfiles_store)
148
+ @cloudfiles.send(:save)
149
+ end
150
+
151
+ it "should open local file" do
152
+ add_stubs(:connection, :stat, :create_container, :cloudfiles_store)
153
+ mock(File).open("foo")
154
+ @cloudfiles.send(:save)
155
+ end
156
+
157
+ it "should upload file" do
158
+ add_stubs(:connection, :stat, :create_container, :file_open, :cloudfiles_store)
159
+ @cloudfiles.send(:save)
160
+ end
161
+
162
+ it "should fail on files bigger then 5G" do
163
+ add_stubs(:connection)
164
+ mock(File).stat("foo").stub!.size {5*1024*1024*1024+1}
165
+ mock(STDERR).puts(anything)
166
+ dont_allow(Benchmark).realtime
167
+ @cloudfiles.send(:save)
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,213 @@
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
+ local do
7
+ path "path"
8
+ end
9
+
10
+ s3 do
11
+ key "s3 key"
12
+ secret "secret"
13
+ bucket "bucket"
14
+ path "path1"
15
+ end
16
+
17
+ sftp do
18
+ user "sftp user"
19
+ password "sftp password"
20
+ host "sftp host"
21
+ end
22
+
23
+ gpg do
24
+ key "gpg-key"
25
+ password "astrails"
26
+ end
27
+
28
+ keep do
29
+ local 4
30
+ s3 20
31
+ end
32
+
33
+ mysqldump do
34
+ options "-ceKq --single-transaction --create-options"
35
+
36
+ user "astrails"
37
+ password ""
38
+ host "localhost"
39
+ port 3306
40
+ socket "/var/run/mysqld/mysqld.sock"
41
+
42
+ database :blog
43
+
44
+ database :production do
45
+ keep :local => 3
46
+
47
+ gpg do
48
+ password "custom-production-pass"
49
+ end
50
+
51
+ skip_tables [:logger_exceptions, :request_logs]
52
+ end
53
+
54
+ end
55
+
56
+ pgdump do
57
+ options "-i -x -O"
58
+
59
+ user "astrails"
60
+ password ""
61
+ host "localhost"
62
+ port 5432
63
+
64
+ database :blog
65
+
66
+ database :production do
67
+ keep :local => 3
68
+
69
+ skip_tables [:logger_exceptions, :request_logs]
70
+ end
71
+
72
+ end
73
+
74
+ svndump do
75
+ repo :my_repo do
76
+ repo_path "/home/svn/my_repo"
77
+ end
78
+ end
79
+
80
+ tar do
81
+ archive "git-repositories" do
82
+ files "/home/git/repositories"
83
+ end
84
+
85
+ archive "etc-files" do
86
+ files "/etc"
87
+ exclude "/etc/puppet/other"
88
+ end
89
+
90
+ archive "dot-configs" do
91
+ files "/home/*/.[^.]*"
92
+ end
93
+
94
+ archive "blog" do
95
+ files "/var/www/blog.astrails.com/"
96
+ exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
97
+ end
98
+
99
+ archive :misc do
100
+ files [ "/backup/*.rb" ]
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ expected = {
107
+ "local" => {"path" => "path"},
108
+
109
+ "s3" => {
110
+ "key" => "s3 key",
111
+ "secret" => "secret",
112
+ "bucket" => "bucket",
113
+ "path" => "path1",
114
+ },
115
+
116
+ "sftp" => {
117
+ "user" => "sftp user",
118
+ "password" => "sftp password",
119
+ "host" => "sftp host",
120
+ },
121
+
122
+ "gpg" => {"password" => "astrails", "key" => "gpg-key"},
123
+
124
+ "keep" => {"s3" => 20, "local" => 4},
125
+
126
+ "mysqldump" => {
127
+ "options" => "-ceKq --single-transaction --create-options",
128
+ "user" => "astrails",
129
+ "password" => "",
130
+ "host" => "localhost",
131
+ "port" => 3306,
132
+ "socket" => "/var/run/mysqld/mysqld.sock",
133
+
134
+ "databases" => {
135
+ "blog" => {},
136
+ "production" => {
137
+ "keep" => {"local" => 3},
138
+ "gpg" => {"password" => "custom-production-pass"},
139
+ "skip_tables" => ["logger_exceptions", "request_logs"],
140
+ },
141
+ },
142
+ },
143
+
144
+ "pgdump" => {
145
+ "options" => "-i -x -O",
146
+ "user" => "astrails",
147
+ "password" => "",
148
+ "host" => "localhost",
149
+ "port" => 5432,
150
+
151
+ "databases" => {
152
+ "blog" => {},
153
+ "production" => {
154
+ "keep" => {"local" => 3},
155
+ "skip_tables" => ["logger_exceptions", "request_logs"],
156
+ },
157
+ },
158
+ },
159
+
160
+ "svndump" => {
161
+ "repos" => {
162
+ "my_repo"=> {
163
+ "repo_path" => "/home/svn/my_repo"
164
+ }
165
+ }
166
+ },
167
+
168
+ "tar" => {
169
+ "archives" => {
170
+ "git-repositories" => {"files" => "/home/git/repositories"},
171
+ "etc-files" => {"files" => "/etc", "exclude" => "/etc/puppet/other"},
172
+ "dot-configs" => {"files" => "/home/*/.[^.]*"},
173
+ "blog" => {
174
+ "files" => "/var/www/blog.astrails.com/",
175
+ "exclude" => ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"],
176
+ },
177
+ "misc" => { "files" => ["/backup/*.rb"] },
178
+ },
179
+ },
180
+ }
181
+
182
+ config.to_hash.should == expected
183
+ end
184
+
185
+ it "should make an array from multivalues" do
186
+ config = Astrails::Safe::Config::Node.new do
187
+ skip_tables "a"
188
+ skip_tables "b"
189
+ files "/foo"
190
+ files "/bar"
191
+ exclude "/foo/bar"
192
+ exclude "/foo/bar/baz"
193
+ end
194
+
195
+ expected = {
196
+ "skip_tables" => ["a", "b"],
197
+ "files" => ["/foo", "/bar"],
198
+ "exclude" => ["/foo/bar", "/foo/bar/baz"],
199
+ }
200
+
201
+ config.to_hash.should == expected
202
+ end
203
+
204
+ it "should raise error on key duplication" do
205
+ proc do
206
+ Astrails::Safe::Config::Node.new do
207
+ path "foo"
208
+ path "bar"
209
+ end
210
+ end.should raise_error(ArgumentError, "duplicate value for 'path'")
211
+ end
212
+
213
+ end