lsync 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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