bostonlogic-safe 0.3.0
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/LICENSE +20 -0
- data/README.markdown +227 -0
- data/Rakefile +54 -0
- data/VERSION.yml +4 -0
- data/bin/astrails-safe +53 -0
- data/examples/example_helper.rb +19 -0
- data/examples/integration/archive_integration_example.rb +86 -0
- data/examples/integration/cleanup_example.rb +62 -0
- data/examples/unit/archive_example.rb +67 -0
- data/examples/unit/config_example.rb +184 -0
- data/examples/unit/gpg_example.rb +138 -0
- data/examples/unit/gzip_example.rb +64 -0
- data/examples/unit/local_example.rb +110 -0
- data/examples/unit/mysqldump_example.rb +83 -0
- data/examples/unit/pgdump_example.rb +45 -0
- data/examples/unit/rcloud_example.rb +110 -0
- data/examples/unit/s3_example.rb +112 -0
- data/examples/unit/svndump_example.rb +39 -0
- data/lib/astrails/safe.rb +71 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/backup.rb +20 -0
- data/lib/astrails/safe/config/builder.rb +62 -0
- data/lib/astrails/safe/config/node.rb +66 -0
- data/lib/astrails/safe/gpg.rb +45 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +48 -0
- data/lib/astrails/safe/mysqldump.rb +31 -0
- data/lib/astrails/safe/notification.rb +66 -0
- data/lib/astrails/safe/pgdump.rb +36 -0
- data/lib/astrails/safe/pipe.rb +13 -0
- data/lib/astrails/safe/rcloud.rb +73 -0
- data/lib/astrails/safe/s3.rb +68 -0
- data/lib/astrails/safe/sftp.rb +79 -0
- data/lib/astrails/safe/sink.rb +33 -0
- data/lib/astrails/safe/source.rb +46 -0
- data/lib/astrails/safe/stream.rb +19 -0
- data/lib/astrails/safe/svndump.rb +13 -0
- data/lib/astrails/safe/tmp_file.rb +48 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/templates/script.rb +155 -0
- metadata +135 -0
@@ -0,0 +1,62 @@
|
|
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/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
|
+
puts "Expecting: #{@backup}"
|
51
|
+
File.exists?(@backup).should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should remove old backups" do
|
55
|
+
Dir["#{@dst}/archive/archive-foo.*"].should == [@backup]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should NOT remove backups with base having same prefix" do
|
59
|
+
Dir["#{@dst}/archive/archive-foobar.*"].should == ["#{@dst}/archive/archive-foobar.000001.tar.gz", "#{@dst}/archive/archive-foobar.000002.tar.gz"]
|
60
|
+
end
|
61
|
+
|
62
|
+
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
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_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
|
+
end
|
@@ -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
|
+
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
|
+
before(:each) do
|
106
|
+
@gpg = gpg(:gpg => {:key => "foo", :options => "GPG-OPT"}, :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 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 => "GPG-OPT"}, :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 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
|