lsync 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/lsync +152 -0
- data/lib/lsync.rb +40 -0
- data/lib/lsync/action.rb +100 -0
- data/lib/lsync/actions/darwin/disk +19 -0
- data/lib/lsync/actions/darwin/terminal +8 -0
- data/lib/lsync/actions/generic/prune +251 -0
- data/lib/lsync/actions/generic/rotate +75 -0
- data/lib/lsync/actions/linux/disk +28 -0
- data/lib/lsync/actions/linux/terminal +6 -0
- data/lib/lsync/backup_error.rb +33 -0
- data/lib/lsync/backup_plan.rb +249 -0
- data/lib/lsync/backup_script.rb +136 -0
- data/lib/lsync/directory.rb +39 -0
- data/lib/lsync/extensions.rb +22 -0
- data/lib/lsync/lb.py +1304 -0
- data/lib/lsync/method.rb +191 -0
- data/lib/lsync/password.rb +35 -0
- data/lib/lsync/run.rb +34 -0
- data/lib/lsync/server.rb +94 -0
- data/lib/lsync/shell.rb +103 -0
- data/lib/lsync/shell_client.rb +84 -0
- data/lib/lsync/tee_logger.rb +37 -0
- data/lib/lsync/version.rb +24 -0
- metadata +131 -0
data/lib/lsync/method.rb
ADDED
@@ -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
|
+
|
data/lib/lsync/run.rb
ADDED
@@ -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
|
data/lib/lsync/server.rb
ADDED
@@ -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
|
data/lib/lsync/shell.rb
ADDED
@@ -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
|