colin-safe 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +160 -0
- data/Rakefile +52 -0
- data/VERSION.yml +4 -0
- data/bin/astrails-safe +59 -0
- data/examples/example_helper.rb +19 -0
- data/examples/unit/config_example.rb +126 -0
- data/examples/unit/stream_example.rb +33 -0
- data/lib/astrails/safe.rb +41 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/config/builder.rb +60 -0
- data/lib/astrails/safe/config/node.rb +67 -0
- data/lib/astrails/safe/gpg.rb +42 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +54 -0
- data/lib/astrails/safe/mysqldump.rb +31 -0
- data/lib/astrails/safe/pipe.rb +15 -0
- data/lib/astrails/safe/s3.rb +68 -0
- data/lib/astrails/safe/sink.rb +33 -0
- data/lib/astrails/safe/source.rb +31 -0
- data/lib/astrails/safe/stream.rb +45 -0
- data/lib/astrails/safe/tmp_file.rb +25 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/templates/script.rb +109 -0
- metadata +88 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Astrails Ltd.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
astrails-safe
|
2
|
+
=============
|
3
|
+
|
4
|
+
Simple mysql and filesystem backups with S3 support (with optional encryption)
|
5
|
+
|
6
|
+
Motivation
|
7
|
+
----------
|
8
|
+
|
9
|
+
We needed a backup solution that will satisfy the following requirements:
|
10
|
+
|
11
|
+
* opensource
|
12
|
+
* simple to install and configure
|
13
|
+
* support for simple ‘tar’ backups of directories (with includes/excludes)
|
14
|
+
* support for simple mysqldump of mysql databases
|
15
|
+
* support for symmetric or public key encryption
|
16
|
+
* support for local filesystem and Amazon S3 for storage
|
17
|
+
* support for backup rotation. we don’t want backups filling all the diskspace or cost a fortune on S3
|
18
|
+
|
19
|
+
And since we didn't find any, we wrote our own :)
|
20
|
+
|
21
|
+
Usage
|
22
|
+
-----
|
23
|
+
|
24
|
+
Usage:
|
25
|
+
astrails-safe [OPTIONS] CONFIG_FILE
|
26
|
+
Options:
|
27
|
+
-h, --help This help screen
|
28
|
+
-v, --verbose be verbose, duh!
|
29
|
+
-n, --dry-run just pretend, don't do anything.
|
30
|
+
-L, --local skip S3
|
31
|
+
|
32
|
+
Note: CONFIG_FILE will be created from template if missing
|
33
|
+
|
34
|
+
Encryption
|
35
|
+
----------
|
36
|
+
|
37
|
+
If you want to encrypt your backups you have 2 options:
|
38
|
+
* use simple password encryption
|
39
|
+
* use GPG public key encryption
|
40
|
+
|
41
|
+
For simple password, just add password entry in gpg section.
|
42
|
+
For public key encryption you will need to create a public/secret keypair.
|
43
|
+
|
44
|
+
We recommend to create your GPG keys only on your local machine and then
|
45
|
+
transfer your public key to the server that will do the backups.
|
46
|
+
|
47
|
+
This way the server will only know how to encrypt the backups but only you
|
48
|
+
will be able to decrypt them using the secret key you have locally. Of course
|
49
|
+
you MUST backup your backup encryption key :)
|
50
|
+
We recommend also pringing the hard paper copy of your GPG key 'just in case'.
|
51
|
+
|
52
|
+
The procedure to create and transfer the key is as follows:
|
53
|
+
|
54
|
+
1. run 'gpg --gen-gen' on your local machine and follow onscreen instructions to create the key
|
55
|
+
(you can accept all the defaults).
|
56
|
+
|
57
|
+
2. extract your public key into a file (assuming you used test@example.com as your key email):
|
58
|
+
gpg -a --export test@example.com > test@example.com.pub
|
59
|
+
|
60
|
+
3. transfer public key to the server
|
61
|
+
scp backup@example.com root@example.com:
|
62
|
+
|
63
|
+
4. import public key on the remote system:
|
64
|
+
<pre>
|
65
|
+
$ gpg --import test@example.com.pub
|
66
|
+
gpg: key 45CA9403: public key "Test Backup <test@example.com>" imported
|
67
|
+
gpg: Total number processed: 1
|
68
|
+
gpg: imported: 1
|
69
|
+
</pre>
|
70
|
+
|
71
|
+
5. since we don't keep the secret part of the key on the remote server, gpg has
|
72
|
+
no way to know its yours and can be trusted.
|
73
|
+
To fix that we can sign it with other trusted key, or just directly modify its
|
74
|
+
trust level in gpg (use level 5):
|
75
|
+
<pre>
|
76
|
+
$ gpg --edit-key test@example.com
|
77
|
+
...
|
78
|
+
Command> trust
|
79
|
+
...
|
80
|
+
1 = I don't know or won't say
|
81
|
+
2 = I do NOT trust
|
82
|
+
3 = I trust marginally
|
83
|
+
4 = I trust fully
|
84
|
+
5 = I trust ultimately
|
85
|
+
m = back to the main menu
|
86
|
+
|
87
|
+
Your decision? 5
|
88
|
+
...
|
89
|
+
Command> quit
|
90
|
+
</pre>
|
91
|
+
|
92
|
+
6. export your secret key for backup
|
93
|
+
(we recommend to print it on paper and burn to a CD/DVD and store in a safe place):
|
94
|
+
<pre>
|
95
|
+
$ gpg -a --export-secret-key test@example.com > test@example.com.key
|
96
|
+
</pre>
|
97
|
+
|
98
|
+
|
99
|
+
Example configuration
|
100
|
+
---------------------
|
101
|
+
<pre>
|
102
|
+
safe do
|
103
|
+
local :path => "/backup/:kind/:id"
|
104
|
+
|
105
|
+
s3 do
|
106
|
+
key "...................."
|
107
|
+
secret "........................................"
|
108
|
+
bucket "backup.astrails.com"
|
109
|
+
path "servers/alpha/:kind/:id"
|
110
|
+
end
|
111
|
+
|
112
|
+
gpg do
|
113
|
+
# symmetric encryption key
|
114
|
+
# password "qwe"
|
115
|
+
|
116
|
+
# public GPG key (must be known to GPG, i.e. be on the keyring)
|
117
|
+
key "backup@astrails.com"
|
118
|
+
end
|
119
|
+
|
120
|
+
keep do
|
121
|
+
local 2
|
122
|
+
s3 2
|
123
|
+
end
|
124
|
+
|
125
|
+
mysqldump do
|
126
|
+
options "-ceKq --single-transaction --create-options"
|
127
|
+
|
128
|
+
user "root"
|
129
|
+
password "............"
|
130
|
+
socket "/var/run/mysqld/mysqld.sock"
|
131
|
+
|
132
|
+
database :blog
|
133
|
+
database :servershape
|
134
|
+
database :astrails_com
|
135
|
+
database :secret_project_com
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
tar do
|
140
|
+
archive "git-repositories", :files => "/home/git/repositories"
|
141
|
+
archive "dot-configs", :files => "/home/*/.[^.]*"
|
142
|
+
archive "etc", :files => "/etc", :exclude => "/etc/puppet/other"
|
143
|
+
|
144
|
+
archive "blog-astrails-com" do
|
145
|
+
files "/var/www/blog.astrails.com/"
|
146
|
+
exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
|
147
|
+
end
|
148
|
+
|
149
|
+
archive "astrails-com" do
|
150
|
+
files "/var/www/astrails.com/"
|
151
|
+
exclude ["/var/www/astrails.com/log", "/var/www/astrails.com/tmp"]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
</pre>
|
156
|
+
|
157
|
+
Copyright
|
158
|
+
---------
|
159
|
+
|
160
|
+
Copyright (c) 2009 Astrails Ltd. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
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)"
|
10
|
+
gem.email = "we@astrails.com"
|
11
|
+
gem.homepage = "http://github.com/astrails/safe"
|
12
|
+
gem.authors = ["Astrails Ltd."]
|
13
|
+
gem.files = FileList["[A-Z]*.*", "{bin,examples,generators,lib,rails,spec,test,templates}/**/*", 'Rakefile', 'LICENSE*']
|
14
|
+
|
15
|
+
gem.add_dependency("aws-s3")
|
16
|
+
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'micronaut/rake_task'
|
24
|
+
Micronaut::RakeTask.new(:examples) do |examples|
|
25
|
+
examples.pattern = 'examples/**/*_example.rb'
|
26
|
+
examples.ruby_opts << '-Ilib -Iexamples'
|
27
|
+
end
|
28
|
+
|
29
|
+
Micronaut::RakeTask.new(:rcov) do |examples|
|
30
|
+
examples.pattern = 'examples/**/*_example.rb'
|
31
|
+
examples.rcov_opts = '-Ilib -Iexamples'
|
32
|
+
examples.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
task :default => :examples
|
37
|
+
|
38
|
+
require 'rake/rdoctask'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
if File.exist?('VERSION.yml')
|
41
|
+
config = YAML.load(File.read('VERSION.yml'))
|
42
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
43
|
+
else
|
44
|
+
version = ""
|
45
|
+
end
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "safe #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
52
|
+
|
data/VERSION.yml
ADDED
data/bin/astrails-safe
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
require 'tempfile'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'fileutils'
|
7
|
+
require "aws/s3"
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
#require 'ruby-debug'
|
11
|
+
#$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
12
|
+
|
13
|
+
require 'astrails/safe'
|
14
|
+
include Astrails::Safe
|
15
|
+
|
16
|
+
def die(msg)
|
17
|
+
puts "ERROR: #{msg}"
|
18
|
+
exit 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def usage
|
22
|
+
puts <<-END
|
23
|
+
Usage: astrails-safe [OPTIONS] CONFIG_FILE
|
24
|
+
Options:
|
25
|
+
-h, --help This help screen
|
26
|
+
-v, --verbose be verbose, duh!
|
27
|
+
-n, --dry-run just pretend, don't do anything.
|
28
|
+
-L, --local skip S3
|
29
|
+
|
30
|
+
Note: config file will be created from template if missing
|
31
|
+
END
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_options
|
36
|
+
usage if ARGV.delete("-h") || ARGV.delete("--help")
|
37
|
+
$_VERBOSE = ARGV.delete("-v") || ARGV.delete("--verbose")
|
38
|
+
$DRY_RUN = ARGV.delete("-n") || ARGV.delete("--dry-run")
|
39
|
+
$LOCAL = ARGV.delete("-L") || ARGV.delete("--local")
|
40
|
+
usage unless ARGV.first
|
41
|
+
$CONFIG_FILE_NAME = File.expand_path(ARGV.first)
|
42
|
+
end
|
43
|
+
|
44
|
+
def main
|
45
|
+
process_options
|
46
|
+
|
47
|
+
unless File.exists?($CONFIG_FILE_NAME)
|
48
|
+
die "Missing configuration file. NOT CREATED! Rerun w/o the -n argument to create a template configuration file." if $DRY_RUN
|
49
|
+
|
50
|
+
FileUtils.cp File.join(Astrails::Safe::ROOT, "templates", "script.rb"), $CONFIG_FILE_NAME
|
51
|
+
|
52
|
+
die "Created default #{$CONFIG_FILE_NAME}. Please edit and run again."
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
load($CONFIG_FILE_NAME)
|
57
|
+
end
|
58
|
+
|
59
|
+
main
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'micronaut'
|
3
|
+
require 'ruby-debug'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
|
8
|
+
require 'astrails/safe'
|
9
|
+
|
10
|
+
def not_in_editor?
|
11
|
+
!(ENV.has_key?('TM_MODE') || ENV.has_key?('EMACS') || ENV.has_key?('VIM'))
|
12
|
+
end
|
13
|
+
|
14
|
+
Micronaut.configure do |c|
|
15
|
+
c.color_enabled = not_in_editor?
|
16
|
+
c.filter_run :focused => true
|
17
|
+
c.mock_with :rr
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Config do
|
4
|
+
it "foo" 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
|
+
gpg do
|
18
|
+
key "gpg-key"
|
19
|
+
password "astrails"
|
20
|
+
end
|
21
|
+
|
22
|
+
keep do
|
23
|
+
local 4
|
24
|
+
s3 20
|
25
|
+
end
|
26
|
+
|
27
|
+
mysqldump do
|
28
|
+
options "-ceKq --single-transaction --create-options"
|
29
|
+
|
30
|
+
user "astrails"
|
31
|
+
password ""
|
32
|
+
host "localhost"
|
33
|
+
port 3306
|
34
|
+
socket "/var/run/mysqld/mysqld.sock"
|
35
|
+
|
36
|
+
database :blog
|
37
|
+
|
38
|
+
database :production do
|
39
|
+
keep :local => 3
|
40
|
+
|
41
|
+
gpg do
|
42
|
+
password "custom-production-pass"
|
43
|
+
end
|
44
|
+
|
45
|
+
skip_tables [:logger_exceptions, :request_logs]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
tar do
|
52
|
+
archive "git-repositories" do
|
53
|
+
files "/home/git/repositories"
|
54
|
+
end
|
55
|
+
|
56
|
+
archive "etc-files" do
|
57
|
+
files "/etc"
|
58
|
+
exclude "/etc/puppet/other"
|
59
|
+
end
|
60
|
+
|
61
|
+
archive "dot-configs" do
|
62
|
+
files "/home/*/.[^.]*"
|
63
|
+
end
|
64
|
+
|
65
|
+
archive "blog" do
|
66
|
+
files "/var/www/blog.astrails.com/"
|
67
|
+
exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
|
68
|
+
end
|
69
|
+
|
70
|
+
archive :misc do
|
71
|
+
files [ "/backup/*.rb" ]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
expected = {
|
78
|
+
"local" => {"path" => "path"},
|
79
|
+
|
80
|
+
"s3" => {
|
81
|
+
"key" => "s3 key",
|
82
|
+
"secret" => "secret",
|
83
|
+
"bucket" => "bucket",
|
84
|
+
"path" => "path1",
|
85
|
+
},
|
86
|
+
|
87
|
+
"gpg" => {"password" => "astrails", "key" => "gpg-key"},
|
88
|
+
|
89
|
+
"keep" => {"s3" => 20, "local" => 4},
|
90
|
+
|
91
|
+
"mysqldump" => {
|
92
|
+
"options" => "-ceKq --single-transaction --create-options",
|
93
|
+
"user" => "astrails",
|
94
|
+
"password" => "",
|
95
|
+
"host" => "localhost",
|
96
|
+
"port" => 3306,
|
97
|
+
"socket" => "/var/run/mysqld/mysqld.sock",
|
98
|
+
|
99
|
+
"databases" => {
|
100
|
+
"blog" => {},
|
101
|
+
"production" => {
|
102
|
+
"keep" => {"local" => 3},
|
103
|
+
"gpg" => {"password" => "custom-production-pass"},
|
104
|
+
"skip_tables" => ["logger_exceptions", "request_logs"],
|
105
|
+
},
|
106
|
+
},
|
107
|
+
},
|
108
|
+
"tar" => {
|
109
|
+
"archives" => {
|
110
|
+
"git-repositories" => {"files" => "/home/git/repositories"},
|
111
|
+
"etc-files" => {"files" => "/etc", "exclude" => "/etc/puppet/other"},
|
112
|
+
"dot-configs" => {"files" => "/home/*/.[^.]*"},
|
113
|
+
"blog" => {
|
114
|
+
"files" => "/var/www/blog.astrails.com/",
|
115
|
+
"exclude" => ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"],
|
116
|
+
},
|
117
|
+
"misc" => { "files" => ["/backup/*.rb"] },
|
118
|
+
},
|
119
|
+
},
|
120
|
+
}
|
121
|
+
|
122
|
+
config.to_hash.should == expected
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../example_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Stream do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@parent = Object.new
|
7
|
+
@stream = Astrails::Safe::Stream.new(@parent)
|
8
|
+
@r = rand(10)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.it_delegates_to_parent(prop)
|
12
|
+
it "delegates #{prop} to parent if not set" do
|
13
|
+
mock(@parent).__send__(prop) {@r}
|
14
|
+
@stream.send(prop).should == @r
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.it_delegates_to_parent_with_cache(prop)
|
19
|
+
it_delegates_to_parent(prop)
|
20
|
+
|
21
|
+
it "uses cached value for #{prop}" do
|
22
|
+
dont_allow(@parent).__send__(prop)
|
23
|
+
@stream.instance_variable_set "@#{prop}", @r + 1
|
24
|
+
@stream.send(prop).should == @r + 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it_delegates_to_parent_with_cache :id
|
29
|
+
it_delegates_to_parent_with_cache :config
|
30
|
+
|
31
|
+
it_delegates_to_parent :filename
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'extensions/mktmpdir'
|
2
|
+
require 'astrails/safe/tmp_file'
|
3
|
+
|
4
|
+
require 'astrails/safe/config/node'
|
5
|
+
require 'astrails/safe/config/builder'
|
6
|
+
|
7
|
+
require 'astrails/safe/stream'
|
8
|
+
|
9
|
+
require 'astrails/safe/source'
|
10
|
+
require 'astrails/safe/mysqldump'
|
11
|
+
require 'astrails/safe/archive'
|
12
|
+
|
13
|
+
require 'astrails/safe/pipe'
|
14
|
+
require 'astrails/safe/gpg'
|
15
|
+
require 'astrails/safe/gzip'
|
16
|
+
|
17
|
+
require 'astrails/safe/sink'
|
18
|
+
require 'astrails/safe/local'
|
19
|
+
require 'astrails/safe/s3'
|
20
|
+
|
21
|
+
|
22
|
+
module Astrails
|
23
|
+
module Safe
|
24
|
+
ROOT = File.join(File.dirname(__FILE__), "..", "..")
|
25
|
+
|
26
|
+
def timestamp
|
27
|
+
@timestamp ||= Time.now.strftime("%y%m%d-%H%M")
|
28
|
+
end
|
29
|
+
|
30
|
+
def safe(&block)
|
31
|
+
config = Config::Node.new(&block)
|
32
|
+
#config.dump
|
33
|
+
|
34
|
+
Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases])
|
35
|
+
Astrails::Safe::Archive.run(config[:tar, :archives])
|
36
|
+
|
37
|
+
Astrails::Safe::TmpFile.cleanup
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Archive < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
"tar -cf - #{@config[:options]} #{tar_exclude_files} #{tar_files}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def extension; '.tar'; end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def tar_exclude_files
|
14
|
+
[*@config[:exclude]].compact.map{|x| "--exclude=#{x}"} * " "
|
15
|
+
end
|
16
|
+
|
17
|
+
def tar_files
|
18
|
+
raise RuntimeError, "missing files for tar" unless @config[:files]
|
19
|
+
[*@config[:files]] * " "
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
module Config
|
4
|
+
class Builder
|
5
|
+
COLLECTIONS = %w/database archive/
|
6
|
+
ITEMS = %w/s3 key secret bucket path gpg password keep local mysqldump options
|
7
|
+
user host port socket skip_tables tar files exclude filename/
|
8
|
+
NAMES = COLLECTIONS + ITEMS
|
9
|
+
def initialize(node)
|
10
|
+
@node = node
|
11
|
+
end
|
12
|
+
|
13
|
+
# supported args:
|
14
|
+
# args = [value]
|
15
|
+
# args = [id, data]
|
16
|
+
# args = [data]
|
17
|
+
# id/value - simple values, data - hash
|
18
|
+
def method_missing(sym, *args, &block)
|
19
|
+
return super unless NAMES.include?(sym.to_s)
|
20
|
+
|
21
|
+
# do we have id or value?
|
22
|
+
unless args.first.is_a?(Hash)
|
23
|
+
id_or_value = args.shift # nil for args == []
|
24
|
+
end
|
25
|
+
|
26
|
+
id_or_value = id_or_value.map {|v| v.to_s} if id_or_value.is_a?(Array)
|
27
|
+
|
28
|
+
# do we have data hash?
|
29
|
+
if data = args.shift
|
30
|
+
die "#{sym}: hash expected: #{data.inspect}" unless data.is_a?(Hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
#puts "#{sym}: args=#{args.inspect}, id_or_value=#{id_or_value}, data=#{data.inspect}, block=#{block.inspect}"
|
34
|
+
|
35
|
+
die "#{sym}: unexpected: #{args.inspect}" unless args.empty?
|
36
|
+
die "#{sym}: missing arguments" unless id_or_value || data || block
|
37
|
+
|
38
|
+
if COLLECTIONS.include?(sym.to_s) && id_or_value
|
39
|
+
data ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
if !data && !block
|
43
|
+
# simple value assignment
|
44
|
+
@node[sym] = id_or_value
|
45
|
+
|
46
|
+
elsif id_or_value
|
47
|
+
# collection element with id => create collection node and a subnode in it
|
48
|
+
key = sym.to_s + "s"
|
49
|
+
collection = @node[key] || @node.set(key, {})
|
50
|
+
collection.set(id_or_value, data || {}, &block)
|
51
|
+
|
52
|
+
else
|
53
|
+
# simple subnode
|
54
|
+
@node.set(sym, data || {}, &block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'astrails/safe/config/builder'
|
2
|
+
module Astrails
|
3
|
+
module Safe
|
4
|
+
module Config
|
5
|
+
class Node
|
6
|
+
attr_reader :parent
|
7
|
+
def initialize(parent = nil, data = {}, &block)
|
8
|
+
@parent, @data = parent, {}
|
9
|
+
data.each { |k, v| self[k] = v }
|
10
|
+
Builder.new(self).instance_eval(&block) if block
|
11
|
+
end
|
12
|
+
|
13
|
+
# looks for the path from this node DOWN. will not delegate to parent
|
14
|
+
def get(*path)
|
15
|
+
key = path.shift
|
16
|
+
value = @data[key.to_s]
|
17
|
+
return value if value && path.empty?
|
18
|
+
|
19
|
+
value && value.get(*path)
|
20
|
+
end
|
21
|
+
|
22
|
+
# recursive find
|
23
|
+
# starts at the node and continues to the parent
|
24
|
+
def find(*path)
|
25
|
+
get(*path) || @parent && @parent.find(*path)
|
26
|
+
end
|
27
|
+
alias :[] :find
|
28
|
+
|
29
|
+
def set(key, value, &block)
|
30
|
+
@data[key.to_s] =
|
31
|
+
if value.is_a?(Hash)
|
32
|
+
Node.new(self, value, &block)
|
33
|
+
else
|
34
|
+
raise(ArgumentError, "#{key}: no block supported for simple values") if block
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
alias :[]= :set
|
39
|
+
|
40
|
+
def each(&block)
|
41
|
+
@data.each(&block)
|
42
|
+
end
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
@data.keys.inject({}) do |res, key|
|
47
|
+
value = @data[key]
|
48
|
+
res[key] = value.is_a?(Node) ? value.to_hash : value
|
49
|
+
res
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def dump(indent = "")
|
54
|
+
@data.each do |key, value|
|
55
|
+
if value.is_a?(Node)
|
56
|
+
puts "#{indent}#{key}:"
|
57
|
+
value.dump(indent + " ")
|
58
|
+
else
|
59
|
+
puts "#{indent}#{key}: #{value.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gpg < Pipe
|
4
|
+
|
5
|
+
def compressed?
|
6
|
+
active? || @parent.compressed?
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
if key
|
13
|
+
rise RuntimeError, "can't use both gpg password and pubkey" if password
|
14
|
+
"|gpg -e -r #{key}"
|
15
|
+
elsif password
|
16
|
+
"|gpg -c --passphrase-file #{gpg_password_file(password)}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def extension
|
21
|
+
".gpg" if active?
|
22
|
+
end
|
23
|
+
|
24
|
+
def active?
|
25
|
+
password || key
|
26
|
+
end
|
27
|
+
|
28
|
+
def password
|
29
|
+
@password ||= config[:gpg, :password]
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
@key ||= config[:gpg, :key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def gpg_password_file(pass)
|
37
|
+
return "TEMP_GENERATED_FILENAME" if $DRY_RUN
|
38
|
+
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gzip < Pipe
|
4
|
+
|
5
|
+
def compressed?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
"|gzip" if active?
|
13
|
+
end
|
14
|
+
|
15
|
+
def extension
|
16
|
+
".gz" if active?
|
17
|
+
end
|
18
|
+
|
19
|
+
def active?
|
20
|
+
!@parent.compressed?
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Local < Sink
|
4
|
+
|
5
|
+
def open(&block)
|
6
|
+
return @parent.open(&block) unless active?
|
7
|
+
run
|
8
|
+
File.open(path, &block) unless $DRY_RUN
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def active?
|
14
|
+
# S3 can't upload from pipe. it needs to know file size, so we must pass through :local
|
15
|
+
# will change once we add SSH sink
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def prefix
|
20
|
+
@prefix ||= File.expand_path(expand(@config[:local, :path] || raise(RuntimeError, "missing :local/:path in configuration")))
|
21
|
+
end
|
22
|
+
|
23
|
+
def command
|
24
|
+
"#{@parent.command} > #{path}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
puts "command: #{command}" if $_VERBOSE
|
29
|
+
unless $DRY_RUN
|
30
|
+
FileUtils.mkdir_p(prefix) unless File.directory?(prefix)
|
31
|
+
system command
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def cleanup
|
36
|
+
return unless keep = @config[:keep, :local]
|
37
|
+
|
38
|
+
base = File.basename(filename).split(".").first
|
39
|
+
|
40
|
+
pattern = File.join(prefix, "#{base}*")
|
41
|
+
puts "listing files #{pattern.inspect}" if $_VERBOSE
|
42
|
+
files = Dir[pattern] .
|
43
|
+
select{|f| File.file?(f)} .
|
44
|
+
sort
|
45
|
+
|
46
|
+
cleanup_with_limit(files, keep) do |f|
|
47
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
48
|
+
File.unlink(f) unless $DRY_RUN
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mysqldump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
@commanbd ||= "mysqldump --defaults-extra-file=#{mysql_password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def extension; '.sql'; end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def mysql_password_file
|
14
|
+
Astrails::Safe::TmpFile.create("mysqldump") do |file|
|
15
|
+
file.puts "[mysqldump]"
|
16
|
+
%w/user password socket host port/.each do |k|
|
17
|
+
v = @config[k]
|
18
|
+
file.puts "#{k} = #{v}" if v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def mysql_skip_tables
|
24
|
+
if skip_tables = @config[:skip_tables]
|
25
|
+
[*skip_tables].map { |t| "--ignore-table=#{@id}.#{t}" } * " "
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class S3 < Sink
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def active?
|
8
|
+
bucket && key && secret
|
9
|
+
end
|
10
|
+
|
11
|
+
def prefix
|
12
|
+
@prefix ||= expand(config[:s3, :path] || expand(config[:local, :path] || ":kind/:id"))
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
# needed in cleanup even on dry run
|
17
|
+
AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true) unless $LOCAL
|
18
|
+
|
19
|
+
file = @parent.open
|
20
|
+
puts "Uploading #{bucket}:#{path}" if $_VERBOSE || $DRY_RUN
|
21
|
+
unless $DRY_RUN || $LOCAL
|
22
|
+
AWS::S3::Bucket.create(bucket)
|
23
|
+
AWS::S3::S3Object.store(path, file, bucket)
|
24
|
+
puts "...done" if $_VERBOSE
|
25
|
+
end
|
26
|
+
file.close if file
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def cleanup
|
31
|
+
|
32
|
+
return if $LOCAL
|
33
|
+
|
34
|
+
return unless keep = @config[:keep, :s3]
|
35
|
+
|
36
|
+
bucket = @config[:s3, :bucket]
|
37
|
+
|
38
|
+
base = File.basename(filename).split(".").first
|
39
|
+
|
40
|
+
puts "listing files in #{bucket}:#{prefix}/#{base}"
|
41
|
+
files = AWS::S3::Bucket.objects(bucket, :prefix => "#{prefix}/#{base}", :max_keys => keep * 2)
|
42
|
+
puts files.collect {|x| x.key} if $_VERBOSE
|
43
|
+
|
44
|
+
files = files.
|
45
|
+
collect {|x| x.key}.
|
46
|
+
sort
|
47
|
+
|
48
|
+
cleanup_with_limit(files, keep) do |f|
|
49
|
+
puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
|
50
|
+
AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN || $LOCAL
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def bucket
|
55
|
+
config[:s3, :bucket]
|
56
|
+
end
|
57
|
+
|
58
|
+
def key
|
59
|
+
config[:s3, :key]
|
60
|
+
end
|
61
|
+
|
62
|
+
def secret
|
63
|
+
config[:s3, :secret]
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Sink < Stream
|
4
|
+
|
5
|
+
def run
|
6
|
+
if active?
|
7
|
+
save
|
8
|
+
cleanup
|
9
|
+
else
|
10
|
+
@parent.run
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
# prefix is defined in subclass
|
17
|
+
def path
|
18
|
+
@path ||= File.join(prefix, filename)
|
19
|
+
end
|
20
|
+
|
21
|
+
# call block on files to be removed (all except for the LAST 'limit' files
|
22
|
+
def cleanup_with_limit(files, limit, &block)
|
23
|
+
return unless files.size > limit
|
24
|
+
|
25
|
+
to_remove = files[0..(files.size - limit - 1)]
|
26
|
+
# TODO: validate here
|
27
|
+
to_remove.each(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Source < Stream
|
4
|
+
|
5
|
+
def initialize(id, config)
|
6
|
+
@id, @config = id, config
|
7
|
+
end
|
8
|
+
|
9
|
+
def filename
|
10
|
+
@filename ||= expand(":kind-:id.:timestamp#{extension}")
|
11
|
+
end
|
12
|
+
|
13
|
+
# process each config key as source (with full pipe)
|
14
|
+
def self.run(config)
|
15
|
+
unless config
|
16
|
+
puts "No configuration found for #{human_name}"
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
config.each do |key, value|
|
21
|
+
stream = [Gpg, Gzip, Local, S3].inject(new(key, value)) do |res, klass|
|
22
|
+
klass.new(res)
|
23
|
+
end
|
24
|
+
stream.run
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Stream
|
4
|
+
|
5
|
+
def initialize(parent)
|
6
|
+
@parent = parent
|
7
|
+
end
|
8
|
+
|
9
|
+
def id
|
10
|
+
@id ||= @parent.id
|
11
|
+
end
|
12
|
+
|
13
|
+
def config
|
14
|
+
@config ||= @parent.config
|
15
|
+
end
|
16
|
+
|
17
|
+
def filename
|
18
|
+
@parent.filename
|
19
|
+
end
|
20
|
+
|
21
|
+
def compressed?
|
22
|
+
@parent && @parent.compressed?
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def self.human_name
|
28
|
+
name.split('::').last.downcase
|
29
|
+
end
|
30
|
+
|
31
|
+
def kind
|
32
|
+
@parent ? @parent.kind : self.class.human_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def expand(path)
|
36
|
+
path .
|
37
|
+
gsub(/:kind\b/, kind) .
|
38
|
+
gsub(/:id\b/, id) .
|
39
|
+
gsub(/:timestamp\b/, timestamp)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
module Astrails
|
3
|
+
module Safe
|
4
|
+
module TmpFile
|
5
|
+
@KEEP_FILES = []
|
6
|
+
TMPDIR = Dir.mktmpdir
|
7
|
+
|
8
|
+
def self.cleanup
|
9
|
+
FileUtils.remove_entry_secure TMPDIR
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.create(name)
|
13
|
+
# create temp directory
|
14
|
+
|
15
|
+
file = Tempfile.new(name, TMPDIR)
|
16
|
+
|
17
|
+
yield file
|
18
|
+
|
19
|
+
file.close
|
20
|
+
@KEEP_FILES << file # so that it will not get gcollected and removed from filesystem until the end
|
21
|
+
file.path
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
unless Dir.respond_to?(:mktmpdir)
|
4
|
+
# backward compat for 1.8.6
|
5
|
+
class Dir
|
6
|
+
def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil)
|
7
|
+
case prefix_suffix
|
8
|
+
when nil
|
9
|
+
prefix = "d"
|
10
|
+
suffix = ""
|
11
|
+
when String
|
12
|
+
prefix = prefix_suffix
|
13
|
+
suffix = ""
|
14
|
+
when Array
|
15
|
+
prefix = prefix_suffix[0]
|
16
|
+
suffix = prefix_suffix[1]
|
17
|
+
else
|
18
|
+
raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
|
19
|
+
end
|
20
|
+
tmpdir ||= Dir.tmpdir
|
21
|
+
t = Time.now.strftime("%Y%m%d")
|
22
|
+
n = nil
|
23
|
+
begin
|
24
|
+
path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
|
25
|
+
path << "-#{n}" if n
|
26
|
+
path << suffix
|
27
|
+
Dir.mkdir(path, 0700)
|
28
|
+
rescue Errno::EEXIST
|
29
|
+
n ||= 0
|
30
|
+
n += 1
|
31
|
+
retry
|
32
|
+
end
|
33
|
+
|
34
|
+
if block_given?
|
35
|
+
begin
|
36
|
+
yield path
|
37
|
+
ensure
|
38
|
+
FileUtils.remove_entry_secure path
|
39
|
+
end
|
40
|
+
else
|
41
|
+
path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/templates/script.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
safe do
|
2
|
+
|
3
|
+
# backup file path (not including filename)
|
4
|
+
# supported substitutions:
|
5
|
+
# :kind -> backup 'engine' kind, e.g. "mysqldump" or "archive"
|
6
|
+
# :id -> backup 'id', e.g. "blog", "production", etc.
|
7
|
+
# :timestamp -> current run timestamp (same for all the backups in the same 'run')
|
8
|
+
# you can set separate :path for all backups (or once globally here)
|
9
|
+
local do
|
10
|
+
path "/backup/:kind/"
|
11
|
+
end
|
12
|
+
|
13
|
+
## uncomment to enable uploads to Amazon S3
|
14
|
+
## Amazon S3 auth (optional)
|
15
|
+
## don't forget to add :s3 to the 'store' list
|
16
|
+
# s3 do
|
17
|
+
# key YOUR_S3_KEY
|
18
|
+
# secret YOUR_S3_SECRET
|
19
|
+
# bucket S3_BUCKET
|
20
|
+
# # path for uploads to S3. supports same substitution like :local/:path
|
21
|
+
# path ":kind/" # this is default
|
22
|
+
# end
|
23
|
+
|
24
|
+
## alternative style:
|
25
|
+
# s3 :key => YOUR_S3_KEY, :secret => YOUR_S3_SECRET, :bucket => S3_BUCKET
|
26
|
+
|
27
|
+
## uncomment to enable GPG encryption.
|
28
|
+
## Note: you can use public 'key' or symmetric password but not both!
|
29
|
+
# gpg do
|
30
|
+
# # key "backup@astrails.com"
|
31
|
+
# password "astrails"
|
32
|
+
# end
|
33
|
+
|
34
|
+
## uncomment to enable backup rotation. keep only given number of latest
|
35
|
+
## backups. remove the rest
|
36
|
+
# keep do
|
37
|
+
# local 4 # keep 4 local backups
|
38
|
+
# s3 20 # keep 20 S3 backups
|
39
|
+
# end
|
40
|
+
|
41
|
+
# backup mysql databases with mysqldump
|
42
|
+
mysqldump do
|
43
|
+
# you can override any setting from parent in a child:
|
44
|
+
options "-ceKq --single-transaction --create-options"
|
45
|
+
|
46
|
+
user "astrails"
|
47
|
+
password ""
|
48
|
+
# host "localhost"
|
49
|
+
# port 3306
|
50
|
+
socket "/var/run/mysqld/mysqld.sock"
|
51
|
+
|
52
|
+
# database is a 'collection' element. it must have a hash or block parameter
|
53
|
+
# it will be 'collected' in a 'databases', with database id (1st arg) used as hash key
|
54
|
+
# the following code will create mysqldump/databases/blog and mysqldump/databases/mysql ocnfiguration 'nodes'
|
55
|
+
|
56
|
+
# backup database with default values
|
57
|
+
# database :blog
|
58
|
+
|
59
|
+
# backup overriding some values
|
60
|
+
# database :production do
|
61
|
+
# # you can override 'partially'
|
62
|
+
# keep :local => 3
|
63
|
+
# # keep/local is 3, and keep/s3 is 20 (from parent)
|
64
|
+
|
65
|
+
# # local override for gpg password
|
66
|
+
# gpg do
|
67
|
+
# password "custom-production-pass"
|
68
|
+
# end
|
69
|
+
|
70
|
+
# skip_tables [:logger_exceptions, :request_logs] # skip those tables during backup
|
71
|
+
# end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
tar do
|
77
|
+
# 'archive' is a collection item, just like 'database'
|
78
|
+
# archive "git-repositories" do
|
79
|
+
# # files and directories to backup
|
80
|
+
# files "/home/git/repositories"
|
81
|
+
# end
|
82
|
+
|
83
|
+
# archive "etc-files" do
|
84
|
+
# files "/etc"
|
85
|
+
# # exlude those files/directories
|
86
|
+
# exclude "/etc/puppet/other"
|
87
|
+
# end
|
88
|
+
|
89
|
+
# archive "dot-configs" do
|
90
|
+
# files "/home/*/.[^.]*"
|
91
|
+
# end
|
92
|
+
|
93
|
+
# archive "blog" do
|
94
|
+
# files "/var/www/blog.astrails.com/"
|
95
|
+
# # specify multiple files/directories as array
|
96
|
+
# exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
|
97
|
+
# end
|
98
|
+
|
99
|
+
# archive "site" do
|
100
|
+
# files "/var/www/astrails.com/"
|
101
|
+
# exclude ["/var/www/astrails.com/log", "/var/www/astrails.com/tmp"]
|
102
|
+
# end
|
103
|
+
|
104
|
+
# archive :misc do
|
105
|
+
# files [ "/backup/*.rb" ]
|
106
|
+
# end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: colin-safe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Astrails Ltd.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-16 00:00:00 -07:00
|
13
|
+
default_executable: astrails-safe
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: aws-s3
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: Simple tool to backup MySQL databases and filesystem locally or to Amazon S3 (with optional encryption)
|
26
|
+
email: we@astrails.com
|
27
|
+
executables:
|
28
|
+
- astrails-safe
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.markdown
|
34
|
+
files:
|
35
|
+
- LICENSE
|
36
|
+
- README.markdown
|
37
|
+
- Rakefile
|
38
|
+
- VERSION.yml
|
39
|
+
- bin/astrails-safe
|
40
|
+
- examples/example_helper.rb
|
41
|
+
- examples/unit/config_example.rb
|
42
|
+
- examples/unit/stream_example.rb
|
43
|
+
- lib/astrails/safe.rb
|
44
|
+
- lib/astrails/safe/archive.rb
|
45
|
+
- lib/astrails/safe/config/builder.rb
|
46
|
+
- lib/astrails/safe/config/node.rb
|
47
|
+
- lib/astrails/safe/gpg.rb
|
48
|
+
- lib/astrails/safe/gzip.rb
|
49
|
+
- lib/astrails/safe/local.rb
|
50
|
+
- lib/astrails/safe/mysqldump.rb
|
51
|
+
- lib/astrails/safe/pipe.rb
|
52
|
+
- lib/astrails/safe/s3.rb
|
53
|
+
- lib/astrails/safe/sink.rb
|
54
|
+
- lib/astrails/safe/source.rb
|
55
|
+
- lib/astrails/safe/stream.rb
|
56
|
+
- lib/astrails/safe/tmp_file.rb
|
57
|
+
- lib/extensions/mktmpdir.rb
|
58
|
+
- templates/script.rb
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://github.com/astrails/safe
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options:
|
63
|
+
- --charset=UTF-8
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.2.0
|
82
|
+
signing_key:
|
83
|
+
specification_version: 2
|
84
|
+
summary: Backup filesystem and MySQL to Amazon S3 (with encryption)
|
85
|
+
test_files:
|
86
|
+
- examples/example_helper.rb
|
87
|
+
- examples/unit/stream_example.rb
|
88
|
+
- examples/unit/config_example.rb
|