ralph-safe 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/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 +34 -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 +92 -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,34 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mysqldump < Source
|
4
|
+
|
5
|
+
def command
|
6
|
+
@command ||= "#{path}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
|
+
def path
|
30
|
+
@config[:path] ||= ""
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ralph-safe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Astrails Ltd.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-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
|
+
- README.markdown
|
33
|
+
- LICENSE
|
34
|
+
files:
|
35
|
+
- README.markdown
|
36
|
+
- VERSION.yml
|
37
|
+
- bin/astrails-safe
|
38
|
+
- examples/example_helper.rb
|
39
|
+
- examples/unit
|
40
|
+
- examples/unit/config_example.rb
|
41
|
+
- examples/unit/stream_example.rb
|
42
|
+
- lib/astrails
|
43
|
+
- lib/astrails/safe
|
44
|
+
- lib/astrails/safe/archive.rb
|
45
|
+
- lib/astrails/safe/config
|
46
|
+
- lib/astrails/safe/config/builder.rb
|
47
|
+
- lib/astrails/safe/config/node.rb
|
48
|
+
- lib/astrails/safe/gpg.rb
|
49
|
+
- lib/astrails/safe/gzip.rb
|
50
|
+
- lib/astrails/safe/local.rb
|
51
|
+
- lib/astrails/safe/mysqldump.rb
|
52
|
+
- lib/astrails/safe/pipe.rb
|
53
|
+
- lib/astrails/safe/s3.rb
|
54
|
+
- lib/astrails/safe/sink.rb
|
55
|
+
- lib/astrails/safe/source.rb
|
56
|
+
- lib/astrails/safe/stream.rb
|
57
|
+
- lib/astrails/safe/tmp_file.rb
|
58
|
+
- lib/astrails/safe.rb
|
59
|
+
- lib/extensions
|
60
|
+
- lib/extensions/mktmpdir.rb
|
61
|
+
- templates/script.rb
|
62
|
+
- Rakefile
|
63
|
+
- LICENSE
|
64
|
+
has_rdoc: true
|
65
|
+
homepage: http://github.com/astrails/safe
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --inline-source
|
69
|
+
- --charset=UTF-8
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "0"
|
83
|
+
version:
|
84
|
+
requirements: []
|
85
|
+
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.2.0
|
88
|
+
signing_key:
|
89
|
+
specification_version: 2
|
90
|
+
summary: Backup filesystem and MySQL to Amazon S3 (with encryption)
|
91
|
+
test_files: []
|
92
|
+
|