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
@@ -0,0 +1,132 @@
|
|
1
|
+
|
2
|
+
require 'lsync/method'
|
3
|
+
|
4
|
+
module LSync
|
5
|
+
module Methods
|
6
|
+
|
7
|
+
class RSync < Method
|
8
|
+
protected
|
9
|
+
|
10
|
+
def connect_arguments (local_server, remote_server)
|
11
|
+
# RSync -e option simply appends the hostname. There is no way to control this behaviour.
|
12
|
+
command = remote_server.shell.connection_command(remote_server)
|
13
|
+
|
14
|
+
if command.last != remote_server.host
|
15
|
+
abort "RSync shell requires hostname at end of command! #{cmd.inspect}"
|
16
|
+
else
|
17
|
+
command.pop
|
18
|
+
end
|
19
|
+
|
20
|
+
return ['-e', command.to_cmd]
|
21
|
+
end
|
22
|
+
|
23
|
+
public
|
24
|
+
|
25
|
+
def initialize(direction, options = {})
|
26
|
+
super(options)
|
27
|
+
@direction = direction
|
28
|
+
@command = options[:command] || "rsync"
|
29
|
+
|
30
|
+
@options = options
|
31
|
+
@connection = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def run(master_server, target_server, directory)
|
35
|
+
arguments = (@options[:arguments] || ["--archive"]) + (directory.options[:arguments] || [])
|
36
|
+
|
37
|
+
local_server = nil
|
38
|
+
remote_server = nil
|
39
|
+
|
40
|
+
if @direction == :push
|
41
|
+
local_server = master_server
|
42
|
+
remote_server = target_server
|
43
|
+
|
44
|
+
dst = remote_server.connection_string(directory)
|
45
|
+
src = local_server.full_path(directory)
|
46
|
+
else
|
47
|
+
local_server = target_server
|
48
|
+
remote_server = master_server
|
49
|
+
|
50
|
+
src = remote_server.connection_string(directory)
|
51
|
+
dst = local_server.full_path(directory)
|
52
|
+
end
|
53
|
+
|
54
|
+
arguments += connect_arguments(local_server, remote_server)
|
55
|
+
|
56
|
+
# Create the destination backup directory
|
57
|
+
@connection = target_server.connect
|
58
|
+
@connection.send_object([:mkdir_p, target_server.full_path(directory)])
|
59
|
+
|
60
|
+
@logger.info "In directory #{Dir.getwd}..."
|
61
|
+
Dir.chdir(local_server.root) do
|
62
|
+
if run_handler(src, dst, arguments) == false
|
63
|
+
raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_handler(src, dst, arguments)
|
69
|
+
run_command([@command] + arguments + [src, dst])
|
70
|
+
end
|
71
|
+
|
72
|
+
def should_run?(master_server, current_server, target_server)
|
73
|
+
if @direction == :push
|
74
|
+
return current_server == master_server
|
75
|
+
elsif @direction == :pull
|
76
|
+
return target_server.is_local?
|
77
|
+
else
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def run_command(cmd)
|
83
|
+
return LSync.run_command(cmd, @logger) == 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class RSyncSnapshot < RSync
|
88
|
+
def inprogress_path
|
89
|
+
@options[:inprogress_path] || "backup.inprogress"
|
90
|
+
end
|
91
|
+
|
92
|
+
def run(master_server, target_server, directory)
|
93
|
+
arguments = (@options[:arguments] || []) + (directory.options[:arguments] || [])
|
94
|
+
|
95
|
+
link_dest = Pathname.new("../" * (directory.path.depth + 1)) + "latest" + directory.path
|
96
|
+
arguments += ['--archive', '--link-dest', link_dest.to_s]
|
97
|
+
|
98
|
+
dst_directory = File.join(inprogress_path, directory.to_s)
|
99
|
+
|
100
|
+
local_server = nil
|
101
|
+
remote_server = nil
|
102
|
+
|
103
|
+
if @direction == :push
|
104
|
+
local_server = master_server
|
105
|
+
remote_server = target_server
|
106
|
+
|
107
|
+
dst = remote_server.connection_string(dst_directory)
|
108
|
+
src = local_server.full_path(directory)
|
109
|
+
else
|
110
|
+
local_server = target_server
|
111
|
+
remote_server = master_server
|
112
|
+
|
113
|
+
dst = local_server.full_path(dst_directory)
|
114
|
+
src = remote_server.connection_string(directory)
|
115
|
+
end
|
116
|
+
|
117
|
+
arguments += connect_arguments(local_server, remote_server)
|
118
|
+
|
119
|
+
# Create the destination backup directory
|
120
|
+
@connection = target_server.connect
|
121
|
+
@connection.send_object([:mkdir_p, target_server.full_path(dst_directory)])
|
122
|
+
|
123
|
+
Dir.chdir(local_server.root) do
|
124
|
+
if run_handler(src, dst, arguments) == false
|
125
|
+
raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
data/lib/lsync/run.rb
CHANGED
@@ -2,33 +2,34 @@
|
|
2
2
|
require 'rexec/task'
|
3
3
|
|
4
4
|
module LSync
|
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
|
-
|
33
|
-
|
5
|
+
|
6
|
+
# Run a specific command and output the results to the given logger.
|
7
|
+
def self.run_command(command, logger)
|
8
|
+
logger.info "Running: #{command.to_cmd} in #{Dir.getwd.dump}"
|
9
|
+
|
10
|
+
process_result = RExec::Task.open(command) do |task|
|
11
|
+
task.input.close
|
12
|
+
pipes = [task.output, task.error]
|
13
|
+
|
14
|
+
while pipes.size > 0
|
15
|
+
result = IO.select(pipes)
|
16
|
+
|
17
|
+
result[0].each do |pipe|
|
18
|
+
if pipe.closed? || pipe.eof?
|
19
|
+
pipes.delete(pipe)
|
20
|
+
next
|
21
|
+
end
|
22
|
+
|
23
|
+
if pipe == task.output
|
24
|
+
logger.info pipe.readline.chomp
|
25
|
+
elsif pipe == task.error
|
26
|
+
logger.error pipe.readline.chomp
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
return process_result
|
33
|
+
end
|
34
|
+
|
34
35
|
end
|
data/lib/lsync/script.rb
CHANGED
@@ -7,130 +7,217 @@ require 'lsync/server'
|
|
7
7
|
require 'lsync/directory'
|
8
8
|
|
9
9
|
module LSync
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
10
|
+
|
11
|
+
# The server controller provides event handlers with a unified interface
|
12
|
+
# for dealing with servers and associated actions.
|
13
|
+
class ServerController
|
14
|
+
def initialize(script, server, logger)
|
15
|
+
@script = script
|
16
|
+
@server = server
|
17
|
+
@logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
# The containing script.
|
21
|
+
attr :script
|
22
|
+
|
23
|
+
# The current server.
|
24
|
+
attr :server
|
25
|
+
|
26
|
+
# The output logger.
|
27
|
+
attr :logger
|
28
|
+
|
29
|
+
# Run a given shell script on the server.
|
30
|
+
def run!(*function)
|
31
|
+
action = Action.new(function)
|
32
|
+
action.run_on_server(@server, @logger)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The directory controller provides event handlers with a unified interface
|
37
|
+
# for dealing with a particular backup in a particular directory.
|
38
|
+
class DirectoryController < ServerController
|
39
|
+
def initialize(script, master, server, directory, logger)
|
40
|
+
super(script, server, logger)
|
41
|
+
|
42
|
+
@master = master
|
43
|
+
@directory = directory
|
44
|
+
end
|
45
|
+
|
46
|
+
# The master server where data is being copied from.
|
47
|
+
attr :master
|
48
|
+
|
49
|
+
# The directory that the data is being copied within.
|
50
|
+
attr :directory
|
51
|
+
end
|
52
|
+
|
53
|
+
# The main backup/synchronisation mechanism is the backup script. It specifies all
|
54
|
+
# servers and directories, and these are then combined specifically to produce the
|
55
|
+
# desired data replication behaviour.
|
56
|
+
class Script
|
57
|
+
include EventHandler
|
58
|
+
|
59
|
+
def initialize(options = {}, &block)
|
60
|
+
@logger = options[:logger] || Logger.new($stdout)
|
61
|
+
@method = nil
|
62
|
+
|
63
|
+
@servers = {}
|
64
|
+
@directories = []
|
65
|
+
|
66
|
+
@log = nil
|
67
|
+
|
68
|
+
if block_given?
|
69
|
+
instance_eval &block
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Given a name, find out which server config matches it
|
74
|
+
def find_named_server name
|
75
|
+
if @servers.key? name
|
76
|
+
return @servers[name]
|
77
|
+
else
|
78
|
+
hostname = Socket.gethostbyname(name)[0] rescue name
|
79
|
+
return @servers.values.find { |s| s["host"] == hostname }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
alias :[] :find_named_server
|
84
|
+
|
85
|
+
# Find the master server based on the name #master= specified
|
86
|
+
def find_master_server
|
87
|
+
find_named_server(@master)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Find out the config section for the current server
|
91
|
+
def find_current_server
|
92
|
+
master = find_master_server
|
93
|
+
server = nil
|
94
|
+
|
95
|
+
# Find out if the master server is local...
|
96
|
+
if master.is_local?
|
97
|
+
server = master
|
98
|
+
else
|
99
|
+
# Find a server config that specifies the local host
|
100
|
+
server = @servers.values.find { |s| s.is_local? }
|
101
|
+
end
|
102
|
+
|
103
|
+
return server
|
104
|
+
end
|
105
|
+
|
106
|
+
# Register a server with the backup script.
|
107
|
+
def server(name, &block)
|
108
|
+
case name
|
109
|
+
when Symbol
|
110
|
+
host = "localhost"
|
111
|
+
else
|
112
|
+
host = name.to_s
|
113
|
+
end
|
114
|
+
|
115
|
+
server = Server.new(host)
|
116
|
+
|
117
|
+
yield server if block_given?
|
118
|
+
|
119
|
+
@servers[name] = server
|
120
|
+
end
|
121
|
+
|
122
|
+
# Backup a particular path (or paths).
|
123
|
+
def backup(*paths, &block)
|
124
|
+
paths.each do |path|
|
125
|
+
directory = Directory.new(path)
|
126
|
+
|
127
|
+
yield directory if block_given?
|
128
|
+
|
129
|
+
@directories << directory
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# The script logger which will be provided all events when the script is run.
|
134
|
+
attr :logger, true
|
135
|
+
|
136
|
+
# The master server name (e.g. symbolic or host name)
|
137
|
+
attr :master, true
|
138
|
+
|
139
|
+
# A specific method which will perform the backup (e.g. an isntance of LSync::Method)
|
140
|
+
attr :method, true
|
141
|
+
|
142
|
+
# All servers which are participating in the backup process.
|
143
|
+
attr :servers
|
144
|
+
|
145
|
+
# All directories which may be synchronised.
|
146
|
+
attr :directories
|
147
|
+
|
148
|
+
# Log data (an +IO+) specific to the current script.
|
149
|
+
attr :log
|
150
|
+
|
151
|
+
# Run the backup process for all servers and directories specified.
|
152
|
+
def run!
|
153
|
+
start_time = Time.now
|
154
|
+
|
155
|
+
# We buffer the log data so that if there is an error it is available to the notification sub-system
|
156
|
+
@log = StringIO.new
|
157
|
+
local_logger = Logger.new(@log)
|
158
|
+
local_logger.formatter = MinimalLogFormat.new
|
159
|
+
logger = @logger.tee(local_logger)
|
160
|
+
|
161
|
+
master = find_master_server
|
162
|
+
current = find_current_server
|
163
|
+
|
164
|
+
# At this point we must know the current server or we can't continue
|
165
|
+
if current == nil
|
166
|
+
raise ScriptError.new("Could not determine current server!", :script => self, :master => @master)
|
167
|
+
end
|
168
|
+
|
169
|
+
if master.is_local?
|
170
|
+
logger.info "We are the master server..."
|
171
|
+
else
|
172
|
+
logger.info "We are not the master server..."
|
173
|
+
logger.info "Master server is #{@master}..."
|
174
|
+
end
|
175
|
+
|
176
|
+
master_controller = ServerController.new(self, master, logger)
|
177
|
+
|
178
|
+
self.try do
|
179
|
+
method.try do
|
180
|
+
master.try(master_controller) do
|
181
|
+
logger.info "Running backups for server #{current}..."
|
182
|
+
|
183
|
+
run_backups!(master, current, logger)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end_time = Time.now
|
189
|
+
logger.info "Backup Completed (#{end_time - start_time}s)."
|
190
|
+
end
|
191
|
+
|
192
|
+
protected
|
193
|
+
|
194
|
+
# This function runs the method for each directory and server combination specified.
|
195
|
+
def run_backups!(master, current, logger)
|
196
|
+
@servers.each do |name, server|
|
197
|
+
# S is always a data destination, therefore s can't be @master
|
198
|
+
next if server == master
|
199
|
+
|
200
|
+
# Skip servers that shouldn't be processed
|
201
|
+
unless @method.should_run?(master, current, server)
|
202
|
+
logger.info "\t" + "Skipping".rjust(20) + " : #{s}"
|
203
|
+
next
|
204
|
+
end
|
205
|
+
|
206
|
+
server_controller = ServerController.new(self, server, logger)
|
207
|
+
|
208
|
+
server.try(server_controller) do
|
209
|
+
@directories.each do |directory|
|
210
|
+
directory_controller = DirectoryController.new(self, master, server, directory, logger)
|
211
|
+
|
212
|
+
directory.try(directory_controller) do
|
213
|
+
logger.info "\t" + ("Processing " + directory.to_s).rjust(20) + " : #{server}"
|
214
|
+
|
215
|
+
method.run(master, server, directory)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
135
222
|
|
136
223
|
end
|