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.
- 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
|