lsync 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/lsync +152 -0
- data/lib/lsync.rb +40 -0
- data/lib/lsync/action.rb +100 -0
- data/lib/lsync/actions/darwin/disk +19 -0
- data/lib/lsync/actions/darwin/terminal +8 -0
- data/lib/lsync/actions/generic/prune +251 -0
- data/lib/lsync/actions/generic/rotate +75 -0
- data/lib/lsync/actions/linux/disk +28 -0
- data/lib/lsync/actions/linux/terminal +6 -0
- data/lib/lsync/backup_error.rb +33 -0
- data/lib/lsync/backup_plan.rb +249 -0
- data/lib/lsync/backup_script.rb +136 -0
- data/lib/lsync/directory.rb +39 -0
- data/lib/lsync/extensions.rb +22 -0
- data/lib/lsync/lb.py +1304 -0
- data/lib/lsync/method.rb +191 -0
- data/lib/lsync/password.rb +35 -0
- data/lib/lsync/run.rb +34 -0
- data/lib/lsync/server.rb +94 -0
- data/lib/lsync/shell.rb +103 -0
- data/lib/lsync/shell_client.rb +84 -0
- data/lib/lsync/tee_logger.rb +37 -0
- data/lib/lsync/version.rb +24 -0
- metadata +131 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This script takes a given path, and renames it with the given format.
|
4
|
+
# It then ensures that there is a symlink called "latest" that points
|
5
|
+
# to the renamed directory.
|
6
|
+
|
7
|
+
require 'pathname'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'optparse'
|
10
|
+
|
11
|
+
OPTIONS = {
|
12
|
+
:Format => "%Y.%m.%d-%H.%M.%S",
|
13
|
+
:Latest => "latest",
|
14
|
+
:UseGMT => true,
|
15
|
+
:Destination => nil
|
16
|
+
}
|
17
|
+
|
18
|
+
ARGV.options do |o|
|
19
|
+
script_name = File.basename($0)
|
20
|
+
|
21
|
+
o.set_summary_indent(' ')
|
22
|
+
o.banner = "Usage: #{script_name} [options] [directory]"
|
23
|
+
o.define_head "This script is used to rotate directories."
|
24
|
+
|
25
|
+
o.separator ""
|
26
|
+
o.separator "Help and Copyright information"
|
27
|
+
|
28
|
+
o.on("-f format", String, "Set the format of the rotated directory names. See Time$strftime") do |format|
|
29
|
+
OPTIONS[:Format] = format
|
30
|
+
end
|
31
|
+
|
32
|
+
o.on("-l latest", String, "Set the name for the latest rotation symlink.") do |latest|
|
33
|
+
OPTIONS[:Latest] = latest
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on("-d destination", String, "Set the directory to move rotated backups.") do |destination|
|
37
|
+
OPTIONS[:Destination] = destination
|
38
|
+
end
|
39
|
+
|
40
|
+
o.on_tail("--copy", "Display copyright information") do
|
41
|
+
puts "#{script_name}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
|
42
|
+
puts "See http://www.oriontransfer.co.nz/ for more information."
|
43
|
+
|
44
|
+
exit
|
45
|
+
end
|
46
|
+
|
47
|
+
o.on_tail("-h", "--help", "Show this help message.") do
|
48
|
+
puts o
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
end.parse!
|
52
|
+
|
53
|
+
time = Time.now
|
54
|
+
|
55
|
+
if OPTIONS[:UseUTC]
|
56
|
+
time = time.utc
|
57
|
+
end
|
58
|
+
|
59
|
+
dir = Pathname.new(ARGV[0] || ".inprogress")
|
60
|
+
rotation_dir = Pathname.new(OPTIONS[:Destination] ? OPTIONS[:Destination] : dir.dirname)
|
61
|
+
rotated_name = time.strftime(OPTIONS[:Format])
|
62
|
+
rotated_dir = rotation_dir + rotated_name
|
63
|
+
latest_link = rotation_dir + OPTIONS[:Latest]
|
64
|
+
|
65
|
+
# Move rotated dir
|
66
|
+
FileUtils.mv(dir, rotated_dir)
|
67
|
+
|
68
|
+
# Recreate latest symlink
|
69
|
+
if File.symlink?(latest_link)
|
70
|
+
puts "Removing old latest link..."
|
71
|
+
FileUtils.rm(latest_link)
|
72
|
+
end
|
73
|
+
|
74
|
+
puts "Creating latest symlink to #{rotated_name}"
|
75
|
+
FileUtils.ln_s(rotated_name, latest_link)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
Commands = {
|
4
|
+
"mount" => "mount",
|
5
|
+
"unmount" => "umount"
|
6
|
+
}
|
7
|
+
|
8
|
+
DevicePaths = [
|
9
|
+
"/dev/disk/by-label",
|
10
|
+
"/dev/disk/by-uuid",
|
11
|
+
"/dev"
|
12
|
+
]
|
13
|
+
|
14
|
+
action = ARGV[0]
|
15
|
+
disk_name = ARGV[1]
|
16
|
+
|
17
|
+
mountpoint = File.join('', 'mnt', disk_name)
|
18
|
+
|
19
|
+
if (action == 'mountpoint')
|
20
|
+
puts File.join(mountpoint, ARGV[2..-1])
|
21
|
+
else
|
22
|
+
puts "#{action.capitalize}ing #{mountpoint}..."
|
23
|
+
system Commands[action], mountpoint
|
24
|
+
|
25
|
+
if $?.exitstatus != 0 or $?.exitstatus != 3383
|
26
|
+
exit 5
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
module LSync
|
3
|
+
|
4
|
+
class BackupError < StandardError
|
5
|
+
def initialize(reason, components = {})
|
6
|
+
@reason = reason
|
7
|
+
@components = components
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
@reason
|
12
|
+
end
|
13
|
+
|
14
|
+
attr :reason
|
15
|
+
attr :components
|
16
|
+
end
|
17
|
+
|
18
|
+
class BackupScriptError < BackupError
|
19
|
+
end
|
20
|
+
|
21
|
+
class BackupMethodError < BackupError
|
22
|
+
end
|
23
|
+
|
24
|
+
class ConfigurationError < BackupError
|
25
|
+
end
|
26
|
+
|
27
|
+
class BackupActionError < BackupError
|
28
|
+
def initialize(server, action, exception)
|
29
|
+
super("Backup action failed: #{action} (#{exception.to_s})", :action => action, :exception => exception)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
|
2
|
+
# A backup plan is a rule-based engine to process individual scripts.
|
3
|
+
# Failure and success can be delt with over multiple scripts.
|
4
|
+
|
5
|
+
require 'ruleby'
|
6
|
+
|
7
|
+
module Ruleby
|
8
|
+
def self.engine(name, &block)
|
9
|
+
e = Core::Engine.new
|
10
|
+
yield e if block_given?
|
11
|
+
return e
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module LSync
|
16
|
+
|
17
|
+
BuiltInCommands = {
|
18
|
+
"ping-host" => "ping -c 4 -t 5 -o"
|
19
|
+
}
|
20
|
+
|
21
|
+
module Facts
|
22
|
+
class Initial
|
23
|
+
end
|
24
|
+
|
25
|
+
class StageSucceeded
|
26
|
+
def initialize(stage)
|
27
|
+
@stage = stage
|
28
|
+
puts "Stage Succeeded: #{@stage.name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
attr :stage
|
32
|
+
|
33
|
+
def name
|
34
|
+
@stage.name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class StageFailed
|
39
|
+
def initialize(stage)
|
40
|
+
@stage = stage
|
41
|
+
end
|
42
|
+
|
43
|
+
attr :stage
|
44
|
+
|
45
|
+
def name
|
46
|
+
@stage.name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ScriptSucceeded
|
51
|
+
def initialize(stage, script)
|
52
|
+
@stage = stage
|
53
|
+
@script = script
|
54
|
+
end
|
55
|
+
|
56
|
+
attr :stage
|
57
|
+
attr :script
|
58
|
+
end
|
59
|
+
|
60
|
+
class ScriptFailed
|
61
|
+
def initialize(stage, script)
|
62
|
+
@stage = stage
|
63
|
+
@script = script
|
64
|
+
end
|
65
|
+
|
66
|
+
attr :stage
|
67
|
+
attr :script
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class BackupPlanRulebook < Ruleby::Rulebook
|
72
|
+
include Facts
|
73
|
+
|
74
|
+
def rules
|
75
|
+
#rule [ScriptSucceeded, :m] do |v|
|
76
|
+
# script = v[:m].script
|
77
|
+
# puts "Backup #{script.dump} successful"
|
78
|
+
#end
|
79
|
+
|
80
|
+
rule [ScriptFailed, :m] do |v|
|
81
|
+
script = v[:m].script
|
82
|
+
puts "*** Script #{script} failed"
|
83
|
+
end
|
84
|
+
|
85
|
+
#rule [StageSucceeded, :m] do |v|
|
86
|
+
# stage = v[:m].stage
|
87
|
+
# puts "Stage #{stage.name.dump} successful"
|
88
|
+
#end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class StageRulebook < Ruleby::Rulebook
|
93
|
+
include Facts
|
94
|
+
|
95
|
+
def initialize(engine, stage)
|
96
|
+
super(engine)
|
97
|
+
@stage = stage
|
98
|
+
end
|
99
|
+
|
100
|
+
def rules
|
101
|
+
# Does this stage have any rules? (i.e. can it run in any case?)
|
102
|
+
if @stage.rules.size > 0
|
103
|
+
puts "Loading rules for stage #{@stage.name.dump}..."
|
104
|
+
@stage.rules.each do |name, r|
|
105
|
+
puts "\t#{name}..."
|
106
|
+
|
107
|
+
r["when"].each do |s|
|
108
|
+
puts "\t\t#{s.dump}"
|
109
|
+
end
|
110
|
+
|
111
|
+
options = r.dup
|
112
|
+
wh = options.delete("when")
|
113
|
+
|
114
|
+
# Build rule
|
115
|
+
rule("#{@stage.name}_#{name}".to_sym, options, *wh) do |v|
|
116
|
+
@stage.run_scripts
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Bring names into the right scope (i.e. Facts)
|
123
|
+
def __eval__(x)
|
124
|
+
eval(x)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Stage
|
129
|
+
protected
|
130
|
+
RuleConfigKeys = Set.new(["priority", "when"])
|
131
|
+
|
132
|
+
def process_rules config
|
133
|
+
rules = config.keys_matching(/^rule\.(.*)$/)
|
134
|
+
|
135
|
+
if rules.size > 0
|
136
|
+
# Okay
|
137
|
+
elsif config.key? "when"
|
138
|
+
rules = {
|
139
|
+
"rule.default" => config.delete_if { |k,v| !RuleConfigKeys.include?(k) }
|
140
|
+
}
|
141
|
+
else
|
142
|
+
return {}
|
143
|
+
end
|
144
|
+
|
145
|
+
rules.keys.each do |rule_name|
|
146
|
+
options = {}
|
147
|
+
w = rules[rule_name].delete("when") || []
|
148
|
+
w = [w] if w.is_a? String
|
149
|
+
|
150
|
+
rules[rule_name].each { |k,v| options[k.to_sym] = v }
|
151
|
+
rules[rule_name] = options
|
152
|
+
rules[rule_name]["when"] = w.collect { |s| s.gsub('@', '#') }
|
153
|
+
end
|
154
|
+
|
155
|
+
rules
|
156
|
+
end
|
157
|
+
|
158
|
+
def process_scripts config
|
159
|
+
config["scripts"].collect do |s|
|
160
|
+
s.match(/^([^\s]+)(.*)$/)
|
161
|
+
|
162
|
+
if BuiltInCommands.key? $1
|
163
|
+
BuiltInCommands[$1] + $2
|
164
|
+
else
|
165
|
+
s
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
public
|
171
|
+
def initialize(plan, name, config)
|
172
|
+
@plan = plan
|
173
|
+
@name = name
|
174
|
+
|
175
|
+
@scripts = process_scripts(config)
|
176
|
+
@rules = process_rules(config)
|
177
|
+
end
|
178
|
+
|
179
|
+
def run_scripts
|
180
|
+
failed = false
|
181
|
+
|
182
|
+
puts "Running stage #{@name}..."
|
183
|
+
@scripts.each do |script|
|
184
|
+
puts "\tRunning Script #{script}..."
|
185
|
+
|
186
|
+
if system(script)
|
187
|
+
@plan.engine.assert Facts::ScriptSucceeded.new(self, script)
|
188
|
+
else
|
189
|
+
@plan.engine.assert Facts::ScriptFailed.new(self, script)
|
190
|
+
failed = true
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
if failed
|
195
|
+
@plan.engine.assert Facts::StageFailed.new(self)
|
196
|
+
else
|
197
|
+
@plan.engine.assert Facts::StageSucceeded.new(self)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
attr :name
|
202
|
+
attr :scripts
|
203
|
+
attr :rules
|
204
|
+
end
|
205
|
+
|
206
|
+
class BackupPlan
|
207
|
+
def initialize(config, logger = nil)
|
208
|
+
@logger = logger || Logger.new(STDOUT)
|
209
|
+
|
210
|
+
@config = config.keys_matching(/^scripts\.(.*)$/)
|
211
|
+
@stages = config.keys_matching(/^stage\.(.*)$/) { |c,name| Stage.new(self, name, c) }
|
212
|
+
end
|
213
|
+
|
214
|
+
attr :logger, true
|
215
|
+
attr :config
|
216
|
+
|
217
|
+
def run_backup
|
218
|
+
Ruleby.engine :engine do |e|
|
219
|
+
@engine = e
|
220
|
+
|
221
|
+
puts " Loading Rules ".center(80, "=")
|
222
|
+
|
223
|
+
BackupPlanRulebook.new(e).rules
|
224
|
+
|
225
|
+
@stages.each do |k,s|
|
226
|
+
StageRulebook.new(e, s).rules
|
227
|
+
end
|
228
|
+
|
229
|
+
puts " Processing Rules ".center(80, "=")
|
230
|
+
|
231
|
+
e.assert Facts::Initial.new
|
232
|
+
|
233
|
+
e.match
|
234
|
+
@engine = nil
|
235
|
+
end
|
236
|
+
|
237
|
+
puts " Finished ".center(80, "=")
|
238
|
+
end
|
239
|
+
|
240
|
+
attr :engine
|
241
|
+
attr :config
|
242
|
+
attr :stages
|
243
|
+
|
244
|
+
def self.load_from_file(path)
|
245
|
+
new(YAML::load(File.read(path)))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
|
2
|
+
# A backup script coordinates "one" backup as a unit.
|
3
|
+
|
4
|
+
require 'lsync/action'
|
5
|
+
require 'lsync/method'
|
6
|
+
require 'lsync/server'
|
7
|
+
require 'lsync/directory'
|
8
|
+
|
9
|
+
module LSync
|
10
|
+
|
11
|
+
class BackupScript
|
12
|
+
private
|
13
|
+
# Given a name, find out which server config matches it
|
14
|
+
def find_named_server name
|
15
|
+
if @servers.key? name
|
16
|
+
return @servers[name]
|
17
|
+
else
|
18
|
+
hostname = Socket.gethostbyname(name)[0] rescue name
|
19
|
+
return @servers.values.find { |s| s["host"] == hostname }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Find out the config section for the current server
|
24
|
+
def find_current_server
|
25
|
+
server = nil
|
26
|
+
|
27
|
+
# Find out if the master server is local...
|
28
|
+
if @master.is_local?
|
29
|
+
server = @master
|
30
|
+
else
|
31
|
+
# Find a server config that specifies the local host
|
32
|
+
server = @servers.values.find { |s| s.is_local? }
|
33
|
+
end
|
34
|
+
|
35
|
+
return server
|
36
|
+
end
|
37
|
+
|
38
|
+
def script_logger
|
39
|
+
if @config["log-file"]
|
40
|
+
return Logger.new(@config["log-file"], 'weekly')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
public
|
45
|
+
def initialize(config, logger = nil)
|
46
|
+
@config = config
|
47
|
+
|
48
|
+
@logger = logger || Logger.new(STDOUT)
|
49
|
+
|
50
|
+
@servers = config.keys_matching(/^server\./) { |c,n| Server.new(c) }
|
51
|
+
@directories = config.keys_matching(/^directory\./) { |c,n| Directory.new(c) }
|
52
|
+
|
53
|
+
@master = find_named_server(config["master"])
|
54
|
+
|
55
|
+
if @master == nil
|
56
|
+
raise ConfigurationError.new("Could not determine master server!", :script => self)
|
57
|
+
end
|
58
|
+
|
59
|
+
@method = Method.new(config["method"])
|
60
|
+
@log_buffer = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
attr :logger, true
|
64
|
+
attr :master
|
65
|
+
attr :method
|
66
|
+
attr :servers
|
67
|
+
attr :directories
|
68
|
+
attr :log_buffer
|
69
|
+
|
70
|
+
def run_backup
|
71
|
+
# We buffer the log data so that if there is an error it is available to the notification sub-system
|
72
|
+
@log_buffer = StringIO.new
|
73
|
+
logger = @logger.tee(script_logger, Logger.new(@log_buffer))
|
74
|
+
|
75
|
+
current = find_current_server
|
76
|
+
|
77
|
+
# At this point we must know the current server or we can't continue
|
78
|
+
if current == nil
|
79
|
+
raise BackupScriptError.new("Could not determine current server!", :script => self, :master => @master)
|
80
|
+
end
|
81
|
+
|
82
|
+
if @master.is_local?
|
83
|
+
logger.info "We are the master server..."
|
84
|
+
else
|
85
|
+
logger.info "We are not the master server..."
|
86
|
+
logger.info "Master server is #{@master}..."
|
87
|
+
end
|
88
|
+
|
89
|
+
# Run server pre-scripts.. if these fail then we abort the whole backup
|
90
|
+
begin
|
91
|
+
@method.run_actions(:before, logger)
|
92
|
+
@master.run_actions(:before, logger)
|
93
|
+
rescue AbortBackupException
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
97
|
+
logger.info "Running backups for server #{current}..."
|
98
|
+
|
99
|
+
@servers.each do |name, s|
|
100
|
+
# S is always a data destination, therefore s can't be @master
|
101
|
+
next if s == @master
|
102
|
+
|
103
|
+
# Skip servers that shouldn't be processed
|
104
|
+
unless @method.should_run?(@master, current, s)
|
105
|
+
logger.info "\t" + "Skipping".rjust(20) + " : #{s}"
|
106
|
+
next
|
107
|
+
end
|
108
|
+
|
109
|
+
# Run pre-scripts for a particular server
|
110
|
+
begin
|
111
|
+
s.run_actions(:before, logger)
|
112
|
+
rescue AbortBackupException
|
113
|
+
next
|
114
|
+
end
|
115
|
+
|
116
|
+
@directories.each do |name, d|
|
117
|
+
logger.info "\t" + ("Processing " + d.to_s).rjust(20) + " : #{s}"
|
118
|
+
|
119
|
+
@method.logger = logger
|
120
|
+
@method.run(@master, s, d)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Run post-scripts for a particular server
|
124
|
+
s.run_actions(:after, logger)
|
125
|
+
end
|
126
|
+
|
127
|
+
@method.run_actions(:after, logger)
|
128
|
+
@master.run_actions(:after, logger)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.load_from_file(path)
|
132
|
+
new(YAML::load(File.read(path)))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|