lsync 1.2.1
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/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
|