lsync 1.2.5 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +51 -0
- data/lib/lsync.rb +0 -23
- data/lib/lsync/action.rb +97 -92
- data/lib/lsync/actions/darwin/disk +5 -5
- data/lib/lsync/actions/generic/prune +29 -7
- data/lib/lsync/actions/generic/rotate +52 -40
- data/lib/lsync/actions/linux/disk +11 -11
- data/lib/lsync/actions/linux/terminal +2 -0
- data/lib/lsync/directory.rb +49 -35
- data/lib/lsync/error.rb +30 -30
- data/lib/lsync/event_handler.rb +72 -0
- data/lib/lsync/event_timer.rb +80 -0
- data/lib/lsync/method.rb +19 -185
- data/lib/lsync/methods/rsync.rb +132 -0
- data/lib/lsync/run.rb +30 -29
- data/lib/lsync/script.rb +212 -125
- data/lib/lsync/server.rb +77 -92
- data/lib/lsync/shell.rb +58 -97
- data/lib/lsync/shell_client.rb +65 -61
- data/lib/lsync/shells/ssh.rb +47 -0
- data/lib/lsync/tee_logger.rb +44 -31
- data/lib/lsync/version.rb +3 -3
- metadata +25 -58
- data/bin/lsync +0 -142
- data/lib/lsync/extensions.rb +0 -22
- data/lib/lsync/lb.py +0 -1304
- data/lib/lsync/password.rb +0 -35
- data/lib/lsync/plan.rb +0 -249
@@ -1,14 +1,14 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
Commands = {
|
4
|
-
|
5
|
-
|
4
|
+
"mount" => "mount",
|
5
|
+
"unmount" => "umount"
|
6
6
|
}
|
7
7
|
|
8
8
|
DevicePaths = [
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
"/dev/disk/by-label",
|
10
|
+
"/dev/disk/by-uuid",
|
11
|
+
"/dev"
|
12
12
|
]
|
13
13
|
|
14
14
|
action = ARGV[0]
|
@@ -19,10 +19,10 @@ mountpoint = File.join('', 'mnt', disk_name)
|
|
19
19
|
if (action == 'mountpoint')
|
20
20
|
puts File.join(mountpoint, ARGV[2..-1])
|
21
21
|
else
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
28
|
end
|
data/lib/lsync/directory.rb
CHANGED
@@ -1,50 +1,64 @@
|
|
1
1
|
|
2
|
+
require 'lsync/event_handler'
|
2
3
|
require 'pathname'
|
3
4
|
|
4
5
|
class Pathname
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
# "bob" => 1
|
21
|
-
# "bob/dole" => 2
|
22
|
-
# "/bob/dole" => 2
|
6
|
+
# Split a pathname up based on the individual path components.
|
7
|
+
def components
|
8
|
+
return to_s.split(SEPARATOR_PAT)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a trailing slash to the path if it doesn't already exist.
|
12
|
+
def normalize_trailing_slash
|
13
|
+
if to_s.match(/\/$/)
|
14
|
+
return self
|
15
|
+
else
|
16
|
+
return self.class.new(to_s + "/")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the number of path components in a normalised fashion.
|
23
21
|
#
|
22
|
+
# We need to work with a cleanpath to get an accurate depth:
|
23
|
+
# "", "/" => 0
|
24
|
+
# "bob" => 1
|
25
|
+
# "bob/dole" => 2
|
26
|
+
# "/bob/dole" => 2
|
24
27
|
def depth
|
25
28
|
bits = cleanpath.to_s.split(SEPARATOR_PAT)
|
26
|
-
|
29
|
+
|
27
30
|
bits.delete("")
|
28
31
|
bits.delete(".")
|
29
|
-
|
32
|
+
|
30
33
|
return bits.size
|
31
34
|
end
|
32
35
|
end
|
33
36
|
|
34
37
|
module LSync
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
38
|
+
|
39
|
+
# A specific directory which is relative to the root of a given server. Specific configuration details
|
40
|
+
# such as excludes and other options may be specified.
|
41
|
+
class Directory
|
42
|
+
include EventHandler
|
43
|
+
|
44
|
+
def initialize(path)
|
45
|
+
@path = Pathname.new(path).cleanpath.normalize_trailing_slash
|
46
|
+
@options = {:arguments => []}
|
47
|
+
end
|
48
|
+
|
49
|
+
attr :path
|
50
|
+
attr :options
|
51
|
+
|
52
|
+
# Exclude a specific shell glob pattern.
|
53
|
+
def exclude(pattern)
|
54
|
+
# RSync specific... need to figure out if there is a good way to do this generally.
|
55
|
+
@options[:arguments] += ["--exclude", pattern]
|
56
|
+
end
|
57
|
+
|
58
|
+
# A string representation of the path for logging.
|
59
|
+
def to_s
|
60
|
+
@path.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
50
64
|
end
|
data/lib/lsync/error.rb
CHANGED
@@ -1,33 +1,33 @@
|
|
1
1
|
|
2
2
|
module LSync
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
3
|
+
|
4
|
+
# Base exception class which keeps track of related components.
|
5
|
+
class Error < StandardError
|
6
|
+
def initialize(reason, components = {})
|
7
|
+
@reason = reason
|
8
|
+
@components = components
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@reason
|
13
|
+
end
|
14
|
+
|
15
|
+
attr :reason
|
16
|
+
attr :components
|
17
|
+
end
|
18
|
+
|
19
|
+
# Indicates that there has been a major backup script error.
|
20
|
+
class ScriptError < Error
|
21
|
+
end
|
22
|
+
|
23
|
+
# Indicates that there has been a major backup method error.
|
24
|
+
class BackupMethodError < Error
|
25
|
+
end
|
26
|
+
|
27
|
+
# Indicates that a backup action shell script has failed.
|
28
|
+
class ShellScriptError < Error
|
29
|
+
def initialize(script, return_code)
|
30
|
+
super("Shell script #{script} failed", :return_code => return_code)
|
31
|
+
end
|
32
|
+
end
|
33
33
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
module LSync
|
3
|
+
|
4
|
+
# Basic event handling and delegation.
|
5
|
+
module EventHandler
|
6
|
+
# Register an event handler which may be triggered when an event is fired.
|
7
|
+
def on(event, &block)
|
8
|
+
@events ||= {}
|
9
|
+
|
10
|
+
@events[event] ||= []
|
11
|
+
@events[event] << block
|
12
|
+
end
|
13
|
+
|
14
|
+
# Fire an event which calls all registered event handlers in the order they were defined.
|
15
|
+
def fire(event, *args)
|
16
|
+
handled = false
|
17
|
+
|
18
|
+
if @events && @events[event]
|
19
|
+
@events[event].each do |handler|
|
20
|
+
handled = true
|
21
|
+
handler.call(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
return handled
|
26
|
+
end
|
27
|
+
|
28
|
+
# Try executing a given block of code and fire appropriate events.
|
29
|
+
#
|
30
|
+
# The sequence of events (registered via #on) are as follows:
|
31
|
+
# [+:prepare+] Fired before the block is executed. May call #abort! to cancel execution.
|
32
|
+
# [+:success+] Fired after the block of code has executed without raising an exception.
|
33
|
+
# [+:failure+] Fired if an exception is thrown during normal execution.
|
34
|
+
# [+:done+] Fired at the end of execution regardless of failure.
|
35
|
+
#
|
36
|
+
# If #abort! has been called in the past, this function returns immediately.
|
37
|
+
def try(*arguments)
|
38
|
+
return if @aborted
|
39
|
+
|
40
|
+
begin
|
41
|
+
catch(abort_name) do
|
42
|
+
fire(:prepare, *arguments)
|
43
|
+
|
44
|
+
yield
|
45
|
+
|
46
|
+
fire(:success, *arguments)
|
47
|
+
end
|
48
|
+
rescue Exception => error
|
49
|
+
# Propagage the exception unless it was handled in some specific way.
|
50
|
+
raise unless fire(:failure, *arguments + [error])
|
51
|
+
ensure
|
52
|
+
fire(:done, *arguments)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Abort the current event handler. Aborting an event handler persistently implies that in
|
57
|
+
# the future it will still be aborted; thus calling #try will have no effect.
|
58
|
+
def abort!(persistent = false)
|
59
|
+
@aborted = true if persistent
|
60
|
+
|
61
|
+
throw abort_name
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# The name used for throwing abortions.
|
67
|
+
def abort_name
|
68
|
+
("abort_" + self.class.name).downcase.to_sym
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module LSync
|
5
|
+
|
6
|
+
# Manages a callback that will be executed after a set duration.
|
7
|
+
class Timeout
|
8
|
+
def initialize(timeout, &block)
|
9
|
+
@cancelled = false
|
10
|
+
|
11
|
+
@thread = Thread.new do
|
12
|
+
sleep timeout
|
13
|
+
|
14
|
+
unless @cancelled
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# The thread on which the timeout is being waited.
|
21
|
+
attr :thread
|
22
|
+
|
23
|
+
# Cancel the timeout if possible and ensure that the callback is not executed.
|
24
|
+
def cancel!
|
25
|
+
@cancelled = true
|
26
|
+
@thread.exit
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# The EventTimer provides a simple time based callback mechanism in which events can be aggregated.
|
31
|
+
# If the timer is triggered once, it will take at most max time for the callback to be triggered.
|
32
|
+
class EventTimer
|
33
|
+
# Times are measured in seconds.
|
34
|
+
# Min specifies the minimum duration between callback invocations.
|
35
|
+
# Max specifies the maximum duration between callback invocations.
|
36
|
+
def initialize(max, &block)
|
37
|
+
@max = max
|
38
|
+
|
39
|
+
@fired = nil
|
40
|
+
@timeout = nil
|
41
|
+
|
42
|
+
@callback = Proc.new(&block)
|
43
|
+
@processing = Mutex.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# Trigger the event timer such that within the specified time, the callback will be fired.
|
47
|
+
def trigger!
|
48
|
+
unless @timeout
|
49
|
+
@timeout = Timeout.new(@max) { fire! }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Return true of the timeout has expired, e.g if it has not been fired within the given duration.
|
56
|
+
def expired?(duration = nil)
|
57
|
+
!@fired || ((Time.now - @fired) > duration)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Fire the callback.
|
61
|
+
def fire!
|
62
|
+
@processing.synchronize do
|
63
|
+
@timeout = nil
|
64
|
+
|
65
|
+
@fired = Time.now
|
66
|
+
@callback.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
public
|
71
|
+
|
72
|
+
# Wait for the timeout to complete nicely.
|
73
|
+
def join
|
74
|
+
if @timeout
|
75
|
+
@timeout.thread.join
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/lib/lsync/method.rb
CHANGED
@@ -2,191 +2,25 @@
|
|
2
2
|
require 'fileutils'
|
3
3
|
require 'pathname'
|
4
4
|
require 'lsync/run'
|
5
|
+
require 'lsync/event_handler'
|
5
6
|
|
6
7
|
module LSync
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
if @method == nil
|
27
|
-
raise BackupError.new("Could not find method #{@name}!")
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
attr :logger, true
|
32
|
-
|
33
|
-
def run(master_server, target_server, directory)
|
34
|
-
@method.run(master_server, target_server, directory, @options, @logger)
|
35
|
-
end
|
36
|
-
|
37
|
-
def should_run?(master_server, current_server, target_server)
|
38
|
-
@method.should_run?(master_server, current_server, target_server)
|
39
|
-
end
|
40
|
-
|
41
|
-
def run_actions(actions, logger)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
module Methods
|
46
|
-
module DirectionalMethodHelper
|
47
|
-
protected
|
48
|
-
def connect_options_for_server (local_server, remote_server)
|
49
|
-
# RSync -e option simply appends the hostname. There is no way to control this behaviour.
|
50
|
-
cmd = remote_server.shell.full_command(remote_server)
|
51
|
-
|
52
|
-
if cmd.match(/ #{remote_server.host}$/)
|
53
|
-
cmd.gsub!(/ #{remote_server.host}$/, "")
|
54
|
-
else
|
55
|
-
abort "RSync shell requires hostname at end of command! #{cmd.dump}"
|
56
|
-
end
|
57
|
-
|
58
|
-
['-e', cmd.dump].join(" ")
|
59
|
-
end
|
60
|
-
|
61
|
-
public
|
62
|
-
def initialize(direction)
|
63
|
-
@direction = direction
|
64
|
-
end
|
65
|
-
|
66
|
-
def run(master_server, target_server, directory, options, logger)
|
67
|
-
options ||= ""
|
68
|
-
|
69
|
-
local_server = nil
|
70
|
-
remote_server = nil
|
71
|
-
|
72
|
-
if @direction == :push
|
73
|
-
local_server = master_server
|
74
|
-
remote_server = target_server
|
75
|
-
|
76
|
-
dst = remote_server.connection_string(directory)
|
77
|
-
src = local_server.full_path(directory)
|
78
|
-
else
|
79
|
-
local_server = target_server
|
80
|
-
remote_server = master_server
|
81
|
-
|
82
|
-
src = remote_server.connection_string(directory)
|
83
|
-
dst = local_server.full_path(directory)
|
84
|
-
end
|
85
|
-
|
86
|
-
options += " " + connect_options_for_server(local_server, remote_server)
|
87
|
-
|
88
|
-
# Create the destination backup directory
|
89
|
-
@connection = target_server.connect
|
90
|
-
@connection.send_object([:mkdir_p, target_server.full_path(directory)])
|
91
|
-
|
92
|
-
@logger = logger
|
93
|
-
|
94
|
-
@logger.info "In directory #{Dir.getwd}"
|
95
|
-
Dir.chdir(local_server.root_path) do
|
96
|
-
if run_handler(src, dst, options) == false
|
97
|
-
raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def should_run?(master_server, current_server, target_server)
|
103
|
-
if @direction == :push
|
104
|
-
return current_server == master_server
|
105
|
-
elsif @direction == :pull
|
106
|
-
return target_server.is_local?
|
107
|
-
else
|
108
|
-
return false
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def run_command(cmd)
|
113
|
-
return LSync.run_command(cmd, @logger) == 0
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
class RSync
|
118
|
-
include DirectionalMethodHelper
|
119
|
-
|
120
|
-
def run_handler(src, dst, options)
|
121
|
-
run_command("rsync #{options} #{src.dump} #{dst.dump}")
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
Method.register("rsync-pull", RSync.new(:pull))
|
126
|
-
Method.register("rsync-push", RSync.new(:push))
|
127
|
-
|
128
|
-
class RSyncSnapshot < RSync
|
129
|
-
def run(master_server, target_server, directory, options, logger)
|
130
|
-
options ||= ""
|
131
|
-
link_dest = Pathname.new("../" * (directory.path.depth + 1)) + "latest" + directory.path
|
132
|
-
options += " --archive --link-dest #{link_dest.to_s.dump}"
|
133
|
-
|
134
|
-
inprogress_path = ".inprogress"
|
135
|
-
dst_directory = File.join(inprogress_path, directory.to_s)
|
136
|
-
|
137
|
-
local_server = nil
|
138
|
-
remote_server = nil
|
139
|
-
|
140
|
-
if @direction == :push
|
141
|
-
local_server = master_server
|
142
|
-
remote_server = target_server
|
143
|
-
|
144
|
-
dst = remote_server.connection_string(dst_directory)
|
145
|
-
src = local_server.full_path(directory)
|
146
|
-
else
|
147
|
-
local_server = target_server
|
148
|
-
remote_server = master_server
|
149
|
-
|
150
|
-
dst = local_server.full_path(dst_directory)
|
151
|
-
src = remote_server.connection_string(directory)
|
152
|
-
end
|
153
|
-
|
154
|
-
options += " " + connect_options_for_server(local_server, remote_server)
|
155
|
-
|
156
|
-
# Create the destination backup directory
|
157
|
-
@connection = target_server.connect
|
158
|
-
@connection.send_object([:mkdir_p, target_server.full_path(dst_directory)])
|
159
|
-
|
160
|
-
@logger = logger
|
161
|
-
|
162
|
-
Dir.chdir(local_server.root_path) do
|
163
|
-
if run_handler(src, dst, options) == false
|
164
|
-
raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
Method.register("rsync-snapshot-pull", RSyncSnapshot.new(:pull))
|
171
|
-
Method.register("rsync-snapshot-push", RSyncSnapshot.new(:push))
|
172
|
-
|
173
|
-
class LinkBackup
|
174
|
-
include DirectionalMethodHelper
|
175
|
-
|
176
|
-
def self.lb_bin
|
177
|
-
return File.join(File.dirname(__FILE__), "lb.py")
|
178
|
-
end
|
179
|
-
|
180
|
-
def run_handler(src, dst, options)
|
181
|
-
# Verbose mode for debugging..
|
182
|
-
# options += " --verbose"
|
183
|
-
run_command("python #{LinkBackup.lb_bin.dump} #{options} #{src.dump} #{dst.dump}")
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
Method.register("lb-pull", LinkBackup.new(:pull))
|
188
|
-
Method.register("lb-push", LinkBackup.new(:push))
|
189
|
-
|
190
|
-
end
|
191
|
-
|
8
|
+
|
9
|
+
# A backup method provides the interface to copy data from one system to another.
|
10
|
+
class Method
|
11
|
+
include EventHandler
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@logger = options[:logger] || Logger.new(STDOUT)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr :logger, true
|
18
|
+
|
19
|
+
def run(master_server, target_server, directory)
|
20
|
+
end
|
21
|
+
|
22
|
+
def should_run?(master_server, current_server, target_server)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
192
26
|
end
|