lsync 1.2.5 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def self.run_command(command, logger)
7
- logger.info "Running: #{command} in #{Dir.getwd.dump}"
8
-
9
- process_result = RExec::Task.open(command) do |task|
10
- task.input.close
11
- pipes = [task.output, task.error]
12
-
13
- while pipes.size > 0
14
- result = IO.select(pipes)
15
-
16
- result[0].each do |pipe|
17
- if pipe.closed? || pipe.eof?
18
- pipes.delete(pipe)
19
- next
20
- end
21
-
22
- if pipe == task.output
23
- logger.info pipe.readline.chomp
24
- elsif pipe == task.error
25
- logger.error pipe.readline.chomp
26
- end
27
- end
28
- end
29
- end
30
-
31
- return process_result
32
- end
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
- class Script
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 ScriptError.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
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