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.
@@ -0,0 +1,191 @@
1
+
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'lsync/run'
5
+
6
+ module LSync
7
+
8
+ class Method
9
+ @@methods = {}
10
+
11
+ def self.register(name, handler)
12
+ @@methods[name] = handler
13
+ end
14
+
15
+ def self.lookup(name)
16
+ @@methods[name]
17
+ end
18
+
19
+ def initialize(config, logger = nil)
20
+ @logger = logger || Logger.new(STDOUT)
21
+
22
+ @name, @options = config.split(/\s+/, 2)
23
+
24
+ @method = Method.lookup(@name)
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
+ Dir.chdir(local_server.root_path) do
95
+ if run_handler(src, dst, options) == false
96
+ raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
97
+ end
98
+ end
99
+ end
100
+
101
+ def should_run?(master_server, current_server, target_server)
102
+ if @direction == :push
103
+ return current_server == master_server
104
+ elsif @direction == :pull
105
+ return target_server.is_local?
106
+ else
107
+ return false
108
+ end
109
+ end
110
+
111
+ def run_command(cmd)
112
+ return LSync.run_command(cmd, @logger) == 0
113
+ end
114
+ end
115
+
116
+ class RSync
117
+ include DirectionalMethodHelper
118
+
119
+ def run_handler(src, dst, options)
120
+ run_command("rsync #{options} #{src.dump} #{dst.dump}")
121
+ end
122
+ end
123
+
124
+ Method.register("rsync-pull", RSync.new(:pull))
125
+ Method.register("rsync-push", RSync.new(:push))
126
+
127
+ class RSyncSnapshot < RSync
128
+ def run(master_server, target_server, directory, options, logger)
129
+ options ||= ""
130
+ link_dest = Pathname.new("../" * (directory.depth + 1)) + "latest" + directory.path
131
+ options += " --archive --link-dest #{link_dest.to_s.dump}"
132
+
133
+ inprogress_path = ".inprogress"
134
+ dst_directory = File.join(inprogress_path, directory.to_s)
135
+
136
+ local_server = nil
137
+ remote_server = nil
138
+
139
+ if @direction == :push
140
+ local_server = master_server
141
+ remote_server = target_server
142
+
143
+ dst = remote_server.connection_string(dst_directory)
144
+ src = local_server.full_path(directory)
145
+ else
146
+ local_server = target_server
147
+ remote_server = master_server
148
+
149
+ dst = local_server.full_path(dst_directory)
150
+ src = remote_server.connection_string(directory)
151
+ end
152
+
153
+ options += " " + connect_options_for_server(local_server, remote_server)
154
+
155
+ # Create the destination backup directory
156
+ @connection = target_server.connect
157
+ @connection.send_object([:mkdir_p, target_server.full_path(dst_directory)])
158
+
159
+ @logger = logger
160
+
161
+ Dir.chdir(local_server.root_path) do
162
+ if run_handler(src, dst, options) == false
163
+ raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ Method.register("rsync-snapshot-pull", RSyncSnapshot.new(:pull))
170
+ Method.register("rsync-snapshot-push", RSyncSnapshot.new(:push))
171
+
172
+ class LinkBackup
173
+ include DirectionalMethodHelper
174
+
175
+ def self.lb_bin
176
+ return File.join(File.dirname(__FILE__), "lb.py")
177
+ end
178
+
179
+ def run_handler(src, dst, options)
180
+ # Verbose mode for debugging..
181
+ # options += " --verbose"
182
+ run_command("python #{LinkBackup.lb_bin.dump} #{options} #{src.dump} #{dst.dump}")
183
+ end
184
+ end
185
+
186
+ Method.register("lb-pull", LinkBackup.new(:pull))
187
+ Method.register("lb-push", LinkBackup.new(:push))
188
+
189
+ end
190
+
191
+ end
@@ -0,0 +1,35 @@
1
+
2
+ require 'termios'
3
+
4
+ module Password
5
+ def self.echo(on=true, masked=false)
6
+ term = Termios::getattr( $stdin )
7
+
8
+ if on
9
+ term.c_lflag |= ( Termios::ECHO | Termios::ICANON )
10
+ else # off
11
+ term.c_lflag &= ~Termios::ECHO
12
+ term.c_lflag &= ~Termios::ICANON if masked
13
+ end
14
+
15
+ Termios::setattr( $stdin, Termios::TCSANOW, term )
16
+ end
17
+
18
+ def self.get(message="Password: ")
19
+ begin
20
+ if $stdin.tty?
21
+ echo false
22
+ print message if message
23
+ end
24
+
25
+ pw = $stdin.gets
26
+ pw.chomp!
27
+ ensure
28
+ if $stdin.tty?
29
+ echo true
30
+ print "\n"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,34 @@
1
+
2
+ require 'rexec/task'
3
+
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
+
34
+ end
@@ -0,0 +1,94 @@
1
+
2
+ require 'lsync/shell'
3
+
4
+ module LSync
5
+ class Server
6
+ def initialize(config)
7
+ @host = config["host"] || "localhost"
8
+ @root = config["root"] || "/"
9
+
10
+ @actions = {
11
+ :before => (config["before"] || []).collect { |c| Action.new(c) },
12
+ :after => (config["after"] || []).collect { |c| Action.new(c) }
13
+ }
14
+
15
+ @shell = Shell.new(config["shell"])
16
+
17
+ @enabled = true
18
+
19
+ @connection = nil
20
+ @pid = nil
21
+ end
22
+
23
+ def full_path(directory = "./")
24
+ p = File.expand_path(directory.to_s, root_path)
25
+
26
+ return Pathname.new(p).cleanpath.normalize_trailing_slash.to_s
27
+ end
28
+
29
+ def connection_string(directory)
30
+ if is_local?
31
+ return full_path(directory)
32
+ else
33
+ return @host + ":" + full_path(directory).dump
34
+ end
35
+ end
36
+
37
+ def is_local?
38
+ return true if @host == "localhost"
39
+
40
+ hostname = Socket.gethostname
41
+
42
+ begin
43
+ hostname = Socket.gethostbyname(hostname)[0]
44
+ rescue SocketError
45
+ puts $!
46
+ end
47
+
48
+ return @host == hostname
49
+ end
50
+
51
+ def to_s
52
+ "#{@host}:#{full_path}"
53
+ end
54
+
55
+ def should_run?
56
+ return @enabled
57
+ end
58
+
59
+ def connect
60
+ unless @connection
61
+ @connection, @pid = @shell.connect(self)
62
+ end
63
+
64
+ return @connection
65
+ end
66
+
67
+ attr :host
68
+ attr :shell
69
+ attr :root
70
+
71
+ def root_path
72
+ if @root == nil
73
+ return "/"
74
+ else
75
+ return @root.to_s
76
+ end
77
+ end
78
+
79
+ def run_actions(actions, logger)
80
+ actions = @actions[actions] if actions.class == Symbol
81
+ return if actions.size == 0
82
+
83
+ logger.info "Running #{actions.size} action(s):"
84
+
85
+ actions.each do |a|
86
+ begin
87
+ a.run_on_server(self, logger)
88
+ rescue StandardError
89
+ raise BackupActionError.new(self, a, $!)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,103 @@
1
+
2
+ require 'rexec'
3
+ require 'pathname'
4
+
5
+ module LSync
6
+ CLIENT_CODE = (Pathname.new(__FILE__).dirname + "shell_client.rb").read
7
+
8
+ class Shell
9
+ # Command can be something like "ssh -l username -p 110"
10
+ def initialize(config)
11
+ if config.kind_of? String
12
+ @command = config
13
+ @options = {}
14
+ elsif config.kind_of? Hash
15
+ @command = config["command"]
16
+ @options = config.dup
17
+ end
18
+
19
+ @command ||= "ssh $OPTIONS $HOST"
20
+ @options ||= {}
21
+
22
+ if @command.match(/([^\s]+)/)
23
+ @name = $1
24
+ else
25
+ @name = nil
26
+ end
27
+ end
28
+
29
+ def command_options
30
+ args = []
31
+
32
+ if @name == "ssh"
33
+ @options.each do |k,v|
34
+ case(k.to_sym)
35
+ when :port
36
+ args += ['-p', v.to_i]
37
+ when :key
38
+ args += ['-i', v.dump]
39
+ when :keys
40
+ v.each { |key_path| args += ['-i', key_path.dump] }
41
+ when :timeout
42
+ args += ['-o', "ConnectTimeout #{v.to_i}".dump]
43
+ when :compression
44
+ args += ['-C']
45
+ when :user
46
+ args += ['-l', v.to_s.dump]
47
+ # args += ['-o', "User #{v.to_s}".dump]
48
+ end
49
+ end
50
+ end
51
+
52
+ return args.join(" ")
53
+ end
54
+
55
+ def ruby_path
56
+ @options["ruby"] || "ruby"
57
+ end
58
+
59
+ def full_command(server = nil)
60
+ cmd = @command.dup
61
+
62
+ cmd.gsub!("$OPTIONS", command_options)
63
+
64
+ if server
65
+ cmd.gsub!("$HOST", server.host)
66
+ cmd.gsub!("$ROOT", server.root_path)
67
+ end
68
+
69
+ return cmd
70
+ end
71
+
72
+ protected
73
+ # Return a connection object representing a connection to the given server.
74
+ def open_connection(server)
75
+ if server.is_local?
76
+ $stderr.puts "Opening connection to #{ruby_path.dump}"
77
+ return RExec::start_server(CLIENT_CODE, ruby_path, :passthrough => [])
78
+ else
79
+ command = full_command(server) + " " + ruby_path.dump
80
+ $stderr.puts "Opening connection to #{command}"
81
+ return RExec::start_server(CLIENT_CODE, command, :passthrough => [])
82
+ end
83
+ end
84
+
85
+ public
86
+ def connect(server)
87
+ begin
88
+ connection, pid = open_connection(server)
89
+ message = connection.receive_object
90
+ ensure
91
+ connection.dump_errors
92
+ end
93
+
94
+ abort "Remote shell connection was not successful: #{message}" unless message == :ready
95
+
96
+ return connection, pid
97
+ end
98
+
99
+ attr :command
100
+ attr :name
101
+ end
102
+
103
+ end