lsync 1.2.5 → 2.0.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/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
|