backupgemsteven 0.1.2
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/CHANGELOG +48 -0
- data/Rakefile +26 -0
- data/TODO +7 -0
- data/bin/backup +12 -0
- data/bin/commands.sh +2 -0
- data/doc/LICENSE-GPL.txt +280 -0
- data/doc/index.html +716 -0
- data/doc/styles.css +157 -0
- data/examples/global.rb +28 -0
- data/examples/mediawiki.rb +24 -0
- data/examples/mediawiki_numeric.rb +19 -0
- data/examples/s3.rb +35 -0
- data/lib/backup.rb +23 -0
- data/lib/backup/actor.rb +208 -0
- data/lib/backup/cli.rb +144 -0
- data/lib/backup/configuration.rb +137 -0
- data/lib/backup/date_parser.rb +37 -0
- data/lib/backup/extensions.rb +17 -0
- data/lib/backup/recipes/standard.rb +113 -0
- data/lib/backup/rotator.rb +219 -0
- data/lib/backup/s3_helpers.rb +140 -0
- data/lib/backup/ssh_helpers.rb +135 -0
- data/lib/backup/state_recorder.rb +21 -0
- data/tests/actor_test.rb +69 -0
- data/tests/cleanup.sh +2 -0
- data/tests/optional/s3_test.rb +40 -0
- data/tests/rotation_test.rb +31 -0
- data/tests/s3_test.rb +40 -0
- data/tests/ssh_test.rb +21 -0
- data/tests/tests_helper.rb +5 -0
- metadata +165 -0
data/doc/styles.css
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
/* Layout */
|
2
|
+
html,body{margin:0;padding:0}
|
3
|
+
body{font: 100% arial,sans-serif}
|
4
|
+
p{margin:0 10px 10px}
|
5
|
+
a{color: #981793;}
|
6
|
+
div#header h1{height:80px;line-height:80px;margin:0;
|
7
|
+
padding-left:10px;background: #EEE;color: #79B30B}
|
8
|
+
div#content p{line-height:1.4}
|
9
|
+
div#navigation{background:#B9CAFF}
|
10
|
+
div#extra{background:#FF8539}
|
11
|
+
div#footer{background: #333;color: #FFF}
|
12
|
+
div#footer p{margin:0;padding:5px 10px}
|
13
|
+
div#wrapper{float:right;width:100%;margin-left:-205px}
|
14
|
+
div#content{margin-left:205px}
|
15
|
+
|
16
|
+
div#navigation {
|
17
|
+
float:left;
|
18
|
+
width:200px;
|
19
|
+
margin-right: 5px;
|
20
|
+
}
|
21
|
+
|
22
|
+
div#extra{float:left;clear:left;width:200px}
|
23
|
+
div#footer{clear:both;width:100%}
|
24
|
+
|
25
|
+
|
26
|
+
/* Styles */
|
27
|
+
body {
|
28
|
+
background-color: white;
|
29
|
+
color: #222;
|
30
|
+
font-family: Georgia, Times, serif;
|
31
|
+
line-height: 110%;
|
32
|
+
}
|
33
|
+
|
34
|
+
/* Code styling */
|
35
|
+
.example {
|
36
|
+
border: solid 1px #C9B;
|
37
|
+
background-color: #FFF5F9;
|
38
|
+
padding: 3px;
|
39
|
+
margin: 5px 40px 5px 22px;
|
40
|
+
}
|
41
|
+
|
42
|
+
pre {
|
43
|
+
background-color: #FFFEF9;
|
44
|
+
color: #936;
|
45
|
+
padding: 5px;
|
46
|
+
margin: 0px;
|
47
|
+
line-height: 170%;
|
48
|
+
}
|
49
|
+
|
50
|
+
blockquote code, p code, li code {
|
51
|
+
color: #936;
|
52
|
+
font-style: normal;
|
53
|
+
background-color: #FFFEF9;
|
54
|
+
padding: 2px;
|
55
|
+
line-height: 170%;
|
56
|
+
font-size: 12pt;
|
57
|
+
}
|
58
|
+
|
59
|
+
p code, li code {
|
60
|
+
line-height: 100%;
|
61
|
+
padding: 0px;
|
62
|
+
font-size: 130%;
|
63
|
+
}
|
64
|
+
|
65
|
+
blockquote code {
|
66
|
+
padding: 5px;
|
67
|
+
font-size: .9em;
|
68
|
+
}
|
69
|
+
|
70
|
+
div#content li {
|
71
|
+
margin-bottom: 5px;
|
72
|
+
}
|
73
|
+
|
74
|
+
div.longlist li {
|
75
|
+
padding-bottom: 8px;
|
76
|
+
}
|
77
|
+
|
78
|
+
.last_updated {
|
79
|
+
margin-left: 60%;
|
80
|
+
text-align: right;
|
81
|
+
font-size:80%;
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
/* syntax highlighting */
|
86
|
+
.keyword {
|
87
|
+
font-weight: bold;
|
88
|
+
}
|
89
|
+
.comment {
|
90
|
+
color: #555;
|
91
|
+
}
|
92
|
+
.string, .number {
|
93
|
+
color: #396;
|
94
|
+
}
|
95
|
+
.string {
|
96
|
+
background-color: #E9F5F5;
|
97
|
+
}
|
98
|
+
.regex {
|
99
|
+
background-color: #E9F1F5;
|
100
|
+
color: #435;
|
101
|
+
}
|
102
|
+
.ident {
|
103
|
+
color: #369;
|
104
|
+
}
|
105
|
+
.symbol {
|
106
|
+
color: #000;
|
107
|
+
}
|
108
|
+
.constant, .class {
|
109
|
+
color: #630;
|
110
|
+
font-weight: bold;
|
111
|
+
}
|
112
|
+
|
113
|
+
/* tables */
|
114
|
+
table {
|
115
|
+
margin: 1em 1em 1em 0;
|
116
|
+
background: #f9f9f9;
|
117
|
+
border: 1px #aaaaaa solid;
|
118
|
+
border-collapse: collapse;
|
119
|
+
}
|
120
|
+
|
121
|
+
th, td {
|
122
|
+
border: 1px #aaaaaa solid;
|
123
|
+
padding: 0.2em;
|
124
|
+
}
|
125
|
+
|
126
|
+
th {
|
127
|
+
background: #f2f2f2;
|
128
|
+
text-align: center;
|
129
|
+
}
|
130
|
+
|
131
|
+
/* navigation */
|
132
|
+
div#navigation a, div#extra a {
|
133
|
+
color: #08052B;
|
134
|
+
text-decoration: none;
|
135
|
+
}
|
136
|
+
|
137
|
+
div#navigation a:hover, div#extra a:hover {
|
138
|
+
text-decoration: underline;
|
139
|
+
}
|
140
|
+
|
141
|
+
div#navigation ol, div#extra ul {
|
142
|
+
margin-left: 6px;
|
143
|
+
padding-left: 20px;
|
144
|
+
}
|
145
|
+
|
146
|
+
div#navigation ol ol {
|
147
|
+
padding-left: 20px;
|
148
|
+
}
|
149
|
+
|
150
|
+
div#navigation ol ol li a, ol ol li {
|
151
|
+
color: #424E75;
|
152
|
+
font-size: 90%;
|
153
|
+
}
|
154
|
+
|
155
|
+
.rightad {
|
156
|
+
float: right;
|
157
|
+
}
|
data/examples/global.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Global Settings for Backup
|
3
|
+
# This file sets the global settings for all recipes in the same directory
|
4
|
+
# @author: Nate Murray <nate@natemurray.com>
|
5
|
+
#------------------------------------------------------------------------------
|
6
|
+
|
7
|
+
# This file specifies many, but not all, of the setting you can change for Backup.
|
8
|
+
# Any setting that is set to the default is commented out. Uncomment and
|
9
|
+
# change any to your liking.
|
10
|
+
|
11
|
+
# A single or array of servers to execute the backup tasks on
|
12
|
+
# set :servers, %w{ localhost someotherhost } # default: localhost
|
13
|
+
|
14
|
+
# The directory to place the backup on the backup server
|
15
|
+
set :backup_path, "/var/local/backups/mediawiki"
|
16
|
+
|
17
|
+
# Name of the SSH user
|
18
|
+
# set :ssh_user, ENV['USER']
|
19
|
+
|
20
|
+
# Path to your SSH key
|
21
|
+
# set :identity_key, ENV['HOME'] + "/.ssh/id_rsa"
|
22
|
+
|
23
|
+
# Set global actions
|
24
|
+
# action :compress, :method => :tar_bz2 # tar_bz2 is the default
|
25
|
+
# action :deliver, :method => :mv # mv is the default
|
26
|
+
# action :encrypt, :method => :gpg # gpg is the default
|
27
|
+
|
28
|
+
set :tmp_dir, File.dirname(__FILE__) + "/../tmp"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Mediawiki Backup script
|
3
|
+
# @author: Nate Murray <nate@natemurray.com>
|
4
|
+
#------------------------------------------------------------------------------
|
5
|
+
action(:content) do
|
6
|
+
dump = c[:tmp_dir] + "/test.sql"
|
7
|
+
sh "mysqldump -uroot test > #{dump}"
|
8
|
+
# should something happen here to cleanup the last task? maybe something
|
9
|
+
# should happen to the stack. like if the next task goes through then you
|
10
|
+
# remove he last file from the stack. Ah. if everything goes well you clean
|
11
|
+
# up everything from the stack.
|
12
|
+
dump
|
13
|
+
end
|
14
|
+
|
15
|
+
# action :compress, :method => :tar_bz2 # could be set in global
|
16
|
+
# action :deliver, :method => :scp # could be set in global
|
17
|
+
|
18
|
+
# settings for backup servers are global unless specified otherwise
|
19
|
+
# rotate settings are global unless specified herer
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Mediawiki Backup script
|
3
|
+
# Uses numeric rotation instead of temporal
|
4
|
+
# @author: Nate Murray <nate@natemurray.com>
|
5
|
+
#------------------------------------------------------------------------------
|
6
|
+
action(:content) do
|
7
|
+
dump = c[:tmp_dir] + "/test.sql"
|
8
|
+
sh "mysqldump -uroot test > #{dump}"
|
9
|
+
dump
|
10
|
+
end
|
11
|
+
|
12
|
+
set :rotation_mode, :numeric # base our rotation on numbers not dates
|
13
|
+
set :sons_promoted_after, 3 # upgrade to father after 3 sons
|
14
|
+
set :fathers_promoted_after, 1
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
data/examples/s3.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Example S3 Backup script
|
3
|
+
# @author: Jason L. Perry <jasper@ambethia.com>
|
4
|
+
#------------------------------------------------------------------------------
|
5
|
+
|
6
|
+
# Set the name of the s3 bucket you want to store your backups in.
|
7
|
+
# Your Access ID is prepended to this to avoid naming conflicts.
|
8
|
+
set :backup_path, "database_backup"
|
9
|
+
|
10
|
+
# You can specify your keys here, or set them as environment variables:
|
11
|
+
# AMAZON_ACCESS_KEY_ID
|
12
|
+
# AMAZON_SECRET_ACCESS_KEY
|
13
|
+
set :aws_access, '123'
|
14
|
+
set :aws_secret, 'ABC'
|
15
|
+
|
16
|
+
# S3 does not support renaming objects, so rotation data is stored in an
|
17
|
+
# index. You can specify a different key for index here, if you need to.
|
18
|
+
#
|
19
|
+
# set :rotation_object_key, 'backup_rotation_index.yml'
|
20
|
+
|
21
|
+
action(:content) do
|
22
|
+
dump = c[:tmp_dir] + "/databases.sql"
|
23
|
+
sh "mysqldump -uroot --all-databases > #{dump}"
|
24
|
+
dump
|
25
|
+
end
|
26
|
+
|
27
|
+
action :deliver, :method => :s3
|
28
|
+
action :rotate, :method => :via_s3
|
29
|
+
|
30
|
+
set :son_promoted_on, :fri
|
31
|
+
set :father_promoted_on, :last_fri_of_the_month
|
32
|
+
|
33
|
+
set :sons_to_keep, 7
|
34
|
+
set :fathers_to_keep, 5
|
35
|
+
set :grandfathers_to_keep, 12
|
data/lib/backup.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'runt'
|
3
|
+
require 'date'
|
4
|
+
require 'backup/configuration'
|
5
|
+
require 'backup/extensions'
|
6
|
+
require 'backup/ssh_helpers'
|
7
|
+
require 'backup/date_parser'
|
8
|
+
require 'backup/state_recorder'
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'right_aws'
|
12
|
+
require 'backup/s3_helpers'
|
13
|
+
rescue LoadError
|
14
|
+
# If RightAWS is not installed, no worries, we just
|
15
|
+
# wont have access to s3 methods.
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'madeleine'
|
20
|
+
rescue LoadError
|
21
|
+
# If you don't have madeleine then you just cant use numeric rotation mode
|
22
|
+
::NO_NUMERIC_ROTATION = true
|
23
|
+
end
|
data/lib/backup/actor.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
include FileUtils
|
5
|
+
# An Actor is the entity that actually does the work of determining which
|
6
|
+
# servers should be the target of a particular task, and of executing the
|
7
|
+
# task on each of them in parallel. An Actor is never instantiated
|
8
|
+
# directly--rather, you create a new Configuration instance, and access the
|
9
|
+
# new actor via Configuration#actor.
|
10
|
+
class Actor
|
11
|
+
# The configuration instance associated with this actor.
|
12
|
+
attr_reader :configuration
|
13
|
+
|
14
|
+
# Alias for #configuration
|
15
|
+
alias_method :c, :configuration
|
16
|
+
|
17
|
+
# A hash of the tasks known to this actor, keyed by name. The values are
|
18
|
+
# instances of Actor::Action.
|
19
|
+
attr_reader :action
|
20
|
+
|
21
|
+
# A stack of the results of the actions called
|
22
|
+
attr_reader :result_history
|
23
|
+
|
24
|
+
# the rotator instance
|
25
|
+
attr_reader :rotator
|
26
|
+
|
27
|
+
# Returns an Array of Strings specifying dirty files. These files will be
|
28
|
+
# +rm -rf+ when the +cleanup+ task is called.
|
29
|
+
attr_reader :dirty_files
|
30
|
+
|
31
|
+
class Action #:nodoc:
|
32
|
+
attr_reader :name, :actor, :options
|
33
|
+
|
34
|
+
def initialize(name, actor, options)
|
35
|
+
@name, @actor, @options = name, actor, options
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(config) #:nodoc:
|
40
|
+
@configuration = config
|
41
|
+
@action = {}
|
42
|
+
@result_history = []
|
43
|
+
@dirty_files = []
|
44
|
+
@rotator = Backup::Rotator.new(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
# each action in the action_order is part of the chain. so you start by
|
48
|
+
# setting the output as 'nil' then you try to call before_ action, then
|
49
|
+
# store the output, then cal action with the args if action takes the args
|
50
|
+
# you are sending. if it doesnt give an intelligent error message. do this
|
51
|
+
# for all actions. then call after_action with the output if it exists.
|
52
|
+
# each time out are calilng the method with the arguemtns f the method
|
53
|
+
# exists and the method takes the arguments.
|
54
|
+
def start_process!
|
55
|
+
configuration[:action_order].each do |a|
|
56
|
+
self.send_and_store("before_" + a)
|
57
|
+
self.send_and_store(a)
|
58
|
+
self.send_and_store("after_" + a)
|
59
|
+
end
|
60
|
+
last_result
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_and_store(name)
|
64
|
+
store_result self.send(name) if self.respond_to? name
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define a new task for this actor. The block will be invoked when this
|
68
|
+
# task is called.
|
69
|
+
# todo, this might be more complex if the before and after tasks are going
|
70
|
+
# to be part of the input and output chain
|
71
|
+
def define_action(name, options={}, &block)
|
72
|
+
@action[name] = (options[:action_class] || Action).new(name, self, options)
|
73
|
+
|
74
|
+
if self.respond_to?(name) && !( block_given? || options[:method] )
|
75
|
+
# if it was already defined and we aren't trying to re-define it then
|
76
|
+
# what we are trying to do is define it the same way it is defined now
|
77
|
+
# only with options being sent to it.
|
78
|
+
metaclass.send(:alias_method, "old_#{name}".intern, name)
|
79
|
+
#self.class.send(:alias_method, "old_#{name}".intern, name)
|
80
|
+
#define_method("#{name.to_s}_new".intern) do
|
81
|
+
define_method(name) do
|
82
|
+
begin
|
83
|
+
result = self.send("old_#{name}", options)
|
84
|
+
end
|
85
|
+
result
|
86
|
+
end
|
87
|
+
return
|
88
|
+
end
|
89
|
+
|
90
|
+
define_method(name) do
|
91
|
+
#logger.debug "executing task #{name}"
|
92
|
+
begin
|
93
|
+
if block_given?
|
94
|
+
result = instance_eval( &block )
|
95
|
+
elsif options[:method]
|
96
|
+
#result = self.send(options[:method], options[:args])
|
97
|
+
result = self.send(options[:method])
|
98
|
+
# here we need to have a thing where we can send the arguments
|
99
|
+
# define the method 'content' so that would take the other options
|
100
|
+
# if there are options (any hash) just send along that hash. this needs more work
|
101
|
+
end
|
102
|
+
end
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
def metaclass
|
109
|
+
class << self; self; end
|
110
|
+
end
|
111
|
+
|
112
|
+
# rotate Actions
|
113
|
+
def via_mv; rotator.rotate_via_mv(last_result); end
|
114
|
+
def via_ssh; rotator.rotate_via_ssh(last_result); end
|
115
|
+
def via_ftp; rotator.rotate_via_ftp(last_result); end
|
116
|
+
def via_s3; rotator.rotate_via_s3(last_result); end
|
117
|
+
|
118
|
+
# By default, +:content+ can perform one of three actions
|
119
|
+
# * +:is_file+
|
120
|
+
# * +:is_folder+
|
121
|
+
# + +:is_contents_of+
|
122
|
+
#
|
123
|
+
# Examples:
|
124
|
+
# action :content, :is_file => "/path/to/file" # content is a single file
|
125
|
+
# action :content, :is_folder => "/path/to/folder" # content is the folder itself
|
126
|
+
# action :content, :is_contents_of => "/path/to/other/folder" # files in folder/
|
127
|
+
#
|
128
|
+
# +:is_file+ and +:is_folder+ are basically the same thing in that they
|
129
|
+
# backup the whole file/folder whereas +:is_contents_of+ backs up the
|
130
|
+
# <em>contents</em> of the given folder.
|
131
|
+
#
|
132
|
+
# Note that +:is_contents_of+ performs a very spcific action:
|
133
|
+
# * a temporary directory is created
|
134
|
+
# * all of the files are moved (including subdirectories) into the temporary directory
|
135
|
+
# * the archive is created from the temporary directory
|
136
|
+
#
|
137
|
+
# If you wish to copy the files out of the original directory instead of
|
138
|
+
# moving them. Then you may specify the +copy+ option passing a +true+
|
139
|
+
# value like so:
|
140
|
+
#
|
141
|
+
# action :content, :is_contents_of => "/path/to/other/folder",
|
142
|
+
# :copy => true
|
143
|
+
#
|
144
|
+
# This will copy recursively.
|
145
|
+
#
|
146
|
+
# If this is not your desired behavior then you can easily write your own.
|
147
|
+
# Also, these options only work for local files. If you are getting the
|
148
|
+
# files from a foreign server you will have to write a custom +:content+
|
149
|
+
# method.
|
150
|
+
#
|
151
|
+
def content(opts={})
|
152
|
+
return opts[:is_file] if opts[:is_file]
|
153
|
+
return opts[:is_folder] if opts[:is_folder]
|
154
|
+
if opts[:is_contents_of]
|
155
|
+
orig = opts[:is_contents_of]
|
156
|
+
tmpdir = c[:tmp_dir] + "/tmp_" + Time.now.strftime("%Y%m%d%H%M%S") +"_#{rand}"
|
157
|
+
new_orig = tmpdir + "/" + File.basename(orig)
|
158
|
+
mkdir_p tmpdir
|
159
|
+
mkdir_p new_orig
|
160
|
+
if opts[:copy]
|
161
|
+
cp_r orig + '/.', new_orig
|
162
|
+
else
|
163
|
+
mv orig + '/.', new_orig
|
164
|
+
end
|
165
|
+
dirty_file new_orig
|
166
|
+
return new_orig
|
167
|
+
end
|
168
|
+
if opts[:is_hg_repository]
|
169
|
+
orig = opts[:is_hg_repository]
|
170
|
+
name = opts[:as] || File.basename(orig)
|
171
|
+
new_orig = c[:tmp_dir] + '/' + name
|
172
|
+
sh "hg clone #{orig} #{new_orig}"
|
173
|
+
dirty_file new_orig
|
174
|
+
return new_orig
|
175
|
+
end
|
176
|
+
raise "Unknown option in :content. Try :is_file, :is_folder " +
|
177
|
+
":is_contents_of or :is_hg_repository"
|
178
|
+
end
|
179
|
+
|
180
|
+
# Given name of a file in +string+ adds that file to @dirty_files. These
|
181
|
+
# files will be removed when the +cleanup+ task is called.
|
182
|
+
def dirty_file(string)
|
183
|
+
@dirty_files << string
|
184
|
+
end
|
185
|
+
|
186
|
+
# +cleanup+ takes every element from @dirty_files and performs an +rm -rf+ on the value
|
187
|
+
def cleanup(opts={})
|
188
|
+
dirty_files.each do |f|
|
189
|
+
rm_rf f
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
def define_method(name, &block)
|
195
|
+
metaclass.send(:define_method, name, &block)
|
196
|
+
end
|
197
|
+
|
198
|
+
def store_result(result)
|
199
|
+
@result_history.push result
|
200
|
+
end
|
201
|
+
|
202
|
+
def last_result
|
203
|
+
@result_history.last
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|