lsync 1.2.5 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +51 -0
- data/lib/lsync.rb +0 -23
- data/lib/lsync/action.rb +97 -92
- data/lib/lsync/actions/darwin/disk +5 -5
- data/lib/lsync/actions/generic/prune +29 -7
- data/lib/lsync/actions/generic/rotate +52 -40
- data/lib/lsync/actions/linux/disk +11 -11
- data/lib/lsync/actions/linux/terminal +2 -0
- data/lib/lsync/directory.rb +49 -35
- data/lib/lsync/error.rb +30 -30
- data/lib/lsync/event_handler.rb +72 -0
- data/lib/lsync/event_timer.rb +80 -0
- data/lib/lsync/method.rb +19 -185
- data/lib/lsync/methods/rsync.rb +132 -0
- data/lib/lsync/run.rb +30 -29
- data/lib/lsync/script.rb +212 -125
- data/lib/lsync/server.rb +77 -92
- data/lib/lsync/shell.rb +58 -97
- data/lib/lsync/shell_client.rb +65 -61
- data/lib/lsync/shells/ssh.rb +47 -0
- data/lib/lsync/tee_logger.rb +44 -31
- data/lib/lsync/version.rb +3 -3
- metadata +25 -58
- data/bin/lsync +0 -142
- data/lib/lsync/extensions.rb +0 -22
- data/lib/lsync/lb.py +0 -1304
- data/lib/lsync/password.rb +0 -35
- data/lib/lsync/plan.rb +0 -249
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
LSync
|
2
|
+
=====
|
3
|
+
|
4
|
+
* Author: Samuel G. D. Williams (<http://www.oriontransfer.co.nz>)
|
5
|
+
* Copyright (C) 2009, 2011 Samuel G. D. Williams.
|
6
|
+
* Released under the MIT license.
|
7
|
+
|
8
|
+
LSync is a tool for scripted synchronization and backups. It provides a custom Ruby
|
9
|
+
DSL for describing complex backup and synchronization tasks. It is designed to give
|
10
|
+
maximum flexibility while reducing the complexity of describing complex multi-server
|
11
|
+
setups.
|
12
|
+
|
13
|
+
* Single and multi-server data synchronization.
|
14
|
+
* Incremental backups both locally and remotely.
|
15
|
+
* Backup staging and coordination.
|
16
|
+
* Backup verification using `Fingerprint`.
|
17
|
+
* Data backup redundancy controlled via DNS.
|
18
|
+
|
19
|
+
For examples please see the main [project page][1].
|
20
|
+
|
21
|
+
[1]: http://www.oriontransfer.co.nz/gems/lsync
|
22
|
+
|
23
|
+
Overview
|
24
|
+
--------
|
25
|
+
|
26
|
+
LSync imposes a particular structure regarding the organisation of backup scripts:
|
27
|
+
Backup scripts involve a set of servers and directories. A server is a logical unit
|
28
|
+
where files are available or stored. Directories are specific places within servers.
|
29
|
+
|
30
|
+
License
|
31
|
+
-------
|
32
|
+
|
33
|
+
Copyright (c) 2009, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
34
|
+
|
35
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
36
|
+
of this software and associated documentation files (the "Software"), to deal
|
37
|
+
in the Software without restriction, including without limitation the rights
|
38
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
39
|
+
copies of the Software, and to permit persons to whom the Software is
|
40
|
+
furnished to do so, subject to the following conditions:
|
41
|
+
|
42
|
+
The above copyright notice and this permission notice shall be included in
|
43
|
+
all copies or substantial portions of the Software.
|
44
|
+
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
47
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
48
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
49
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
50
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
51
|
+
THE SOFTWARE.
|
data/lib/lsync.rb
CHANGED
@@ -15,21 +15,13 @@
|
|
15
15
|
|
16
16
|
require 'rubygems'
|
17
17
|
|
18
|
-
gem 'termios'
|
19
|
-
gem 'net-ssh'
|
20
|
-
gem 'ruleby'
|
21
|
-
|
22
18
|
require 'yaml'
|
23
19
|
require 'socket'
|
24
20
|
require 'set'
|
25
21
|
require 'logger'
|
26
22
|
|
27
23
|
require 'lsync/version'
|
28
|
-
require 'lsync/extensions'
|
29
|
-
|
30
24
|
require 'lsync/script'
|
31
|
-
require 'lsync/plan'
|
32
|
-
|
33
25
|
require 'lsync/tee_logger'
|
34
26
|
|
35
27
|
require 'fileutils'
|
@@ -38,19 +30,4 @@ require 'optparse'
|
|
38
30
|
require 'open-uri'
|
39
31
|
|
40
32
|
module LSync
|
41
|
-
class InvalidConfigurationPath < StandardError
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.load_from_file(path)
|
45
|
-
path = Pathname.new(path)
|
46
|
-
|
47
|
-
case path.extname
|
48
|
-
when ".lsync-script"
|
49
|
-
return Script.load_from_file(path)
|
50
|
-
when ".lsync-plan"
|
51
|
-
return Plan.load_from_file(path)
|
52
|
-
else
|
53
|
-
raise InvalidConfigurationPath.new(path)
|
54
|
-
end
|
55
|
-
end
|
56
33
|
end
|
data/lib/lsync/action.rb
CHANGED
@@ -4,97 +4,102 @@ require 'lsync/run'
|
|
4
4
|
require 'lsync/error'
|
5
5
|
|
6
6
|
module LSync
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
7
|
+
|
8
|
+
class AbortBackupException < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
# A runnable action, such as a shell command or action script.
|
12
|
+
#
|
13
|
+
# If the first argument is a symbol, then this will map to one of the standard
|
14
|
+
# actions in the `lsync/actions` subdirectory. These actions are sometimes implemented
|
15
|
+
# on a per-platform basis.
|
16
|
+
class Action
|
17
|
+
def initialize(function)
|
18
|
+
@function = function
|
19
|
+
|
20
|
+
case @function[0]
|
21
|
+
when Symbol
|
22
|
+
@script_name = @function[0].to_s
|
23
|
+
@arguments = @function[1,@function.size]
|
24
|
+
else
|
25
|
+
@script_name = nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return a string representation of the action for logging.
|
30
|
+
def to_s
|
31
|
+
@function.to_cmd
|
32
|
+
end
|
33
|
+
|
34
|
+
# Run the action on the given server, typically in the root directory specified.
|
35
|
+
def run_on_server(server, logger)
|
36
|
+
# logger.info "Running #{@function.to_cmd} on #{server}"
|
37
|
+
|
38
|
+
if server.is_local?
|
39
|
+
run_locally(server, logger)
|
40
|
+
else
|
41
|
+
run_remotely(server, logger)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
# Run the script locally by invoking it directly.
|
47
|
+
def run_locally(server, logger)
|
48
|
+
command = nil
|
49
|
+
|
50
|
+
if @script_name
|
51
|
+
uname = `uname`.chomp.downcase
|
52
|
+
|
53
|
+
local_path = Action.script_path(uname, @script_name)
|
54
|
+
command = [local_path] + @arguments
|
55
|
+
else
|
56
|
+
command = @function
|
57
|
+
end
|
58
|
+
|
59
|
+
result = nil
|
60
|
+
Dir.chdir(server.root) do
|
61
|
+
result = LSync.run_command(command, logger)
|
62
|
+
end
|
63
|
+
|
64
|
+
if result != 0
|
65
|
+
raise ShellScriptError.new(command, result)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Run the script remotely by sending the data across the network and executing it.
|
70
|
+
def run_remotely(server, logger)
|
71
|
+
conn = server.connect
|
72
|
+
conn.send_object([:set_working_dir, server.root])
|
73
|
+
|
74
|
+
if @script_name
|
75
|
+
uname = `uname`.chomp.downcase
|
76
|
+
|
77
|
+
local_path = Action.script_path(uname, @script_name)
|
78
|
+
|
79
|
+
logger.info("Sending run_script #{@script_name}...")
|
80
|
+
conn.send_object([:run_script, @script_name, Pathname.new(local_path).read, @arguments])
|
81
|
+
else
|
82
|
+
logger.info("Sending run_command #{@function}...")
|
83
|
+
conn.send_object([:run_command, @function])
|
84
|
+
end
|
85
|
+
|
86
|
+
conn.run do |message|
|
87
|
+
break if message == :done
|
88
|
+
|
89
|
+
logger.send(*message)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Figure out the path of the script, which may depend on the given platform.
|
94
|
+
def self.script_path(platform, name)
|
95
|
+
exact_script_path(platform, name) || exact_script_path("generic", name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return the exact path of a builtin action script.
|
99
|
+
def self.exact_script_path(platform, name)
|
100
|
+
path = (Pathname.new(__FILE__).dirname + "actions" + platform + name).expand_path
|
101
|
+
path.exist? ? path : nil
|
102
|
+
end
|
103
|
+
end
|
99
104
|
|
100
105
|
end
|
@@ -5,15 +5,15 @@ action = ARGV[0]
|
|
5
5
|
disk_name = ARGV[1]
|
6
6
|
|
7
7
|
def get_disk_id(name)
|
8
|
-
|
9
|
-
|
8
|
+
begin
|
9
|
+
`diskutil list`.match(/#{name}\s*\*?[0-9]+\.[0-9]+ .B\s+(disk[0-9]s[0-9])$/)[1]
|
10
10
|
rescue
|
11
|
-
|
12
|
-
|
11
|
+
exit 5
|
12
|
+
end
|
13
13
|
end
|
14
14
|
|
15
15
|
if (action == 'mountpoint')
|
16
16
|
puts File.join('', 'Volumes', disk_name, ARGV[2..-1])
|
17
17
|
else
|
18
|
-
|
18
|
+
system DISKUTIL, action, get_disk_id(disk_name)
|
19
19
|
end
|
@@ -26,6 +26,23 @@ class Rotation
|
|
26
26
|
def <=> other
|
27
27
|
return other.time <=> @time
|
28
28
|
end
|
29
|
+
|
30
|
+
def eql? other
|
31
|
+
case other
|
32
|
+
when Rotation
|
33
|
+
@path.eql?(other.path) # && @time.eql?(other.time)
|
34
|
+
else
|
35
|
+
@path.eql?(other.to_s)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def hash
|
40
|
+
@path.hash
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
@path
|
45
|
+
end
|
29
46
|
end
|
30
47
|
|
31
48
|
class Period
|
@@ -160,7 +177,7 @@ ARGV.options do |o|
|
|
160
177
|
o.on("--dry-run", "Print out what would be deleted, but don't actually delete anything.") do
|
161
178
|
OPTIONS[:Wet] = false
|
162
179
|
end
|
163
|
-
|
180
|
+
|
164
181
|
o.on("-l latest", String, "Specify the symlink name that points to the latest backup, so it won't be deleted.")
|
165
182
|
|
166
183
|
o.separator ""
|
@@ -225,7 +242,7 @@ end.parse!
|
|
225
242
|
|
226
243
|
backups = []
|
227
244
|
|
228
|
-
Dir.chdir(OPTIONS[:Destination]) do
|
245
|
+
Dir.chdir(OPTIONS[:Destination]) do
|
229
246
|
Dir["*"].each do |path|
|
230
247
|
next if path.match(OPTIONS[:Latest])
|
231
248
|
date_string = File.basename(path)
|
@@ -240,12 +257,17 @@ Dir.chdir(OPTIONS[:Destination]) do
|
|
240
257
|
keep, erase = OPTIONS[:Policy].filter(backups)
|
241
258
|
|
242
259
|
# We need to retain the latest backup regardless of policy
|
243
|
-
if OPTIONS[:Latest]
|
244
|
-
|
245
|
-
|
246
|
-
|
260
|
+
if OPTIONS[:Latest] && File.exist?(OPTIONS[:Latest])
|
261
|
+
path = Pathname.new(OPTIONS[:Latest]).realpath.basename.to_s
|
262
|
+
latest_rotation = erase.find { |rotation| rotation.path == path }
|
263
|
+
|
264
|
+
if latest_rotation
|
265
|
+
puts "Retaining latest backup #{latest_rotation}"
|
266
|
+
erase.delete(latest_rotation)
|
267
|
+
keep << latest_rotation
|
268
|
+
end
|
247
269
|
end
|
248
|
-
|
270
|
+
|
249
271
|
if OPTIONS[:Wet]
|
250
272
|
erase.sort.each do |backup|
|
251
273
|
puts "Erasing #{backup.path}..."
|
@@ -9,66 +9,78 @@ require 'fileutils'
|
|
9
9
|
require 'optparse'
|
10
10
|
|
11
11
|
OPTIONS = {
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
:Format => "%Y.%m.%d-%H.%M.%S",
|
13
|
+
:Latest => "latest",
|
14
|
+
:UseGMT => true,
|
15
|
+
:Destination => nil
|
16
16
|
}
|
17
17
|
|
18
18
|
ARGV.options do |o|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
19
|
+
script_name = File.basename($0)
|
20
|
+
|
21
|
+
o.set_summary_indent(' ')
|
22
|
+
o.banner = "Usage: #{script_name} [options] [directory]"
|
23
|
+
o.define_head "This script is used to rotate directories."
|
24
|
+
|
25
|
+
o.separator ""
|
26
|
+
o.separator "Help and Copyright information"
|
27
|
+
|
28
|
+
o.on("-f format", String, "Set the format of the rotated directory names. See Time$strftime") do |format|
|
29
|
+
OPTIONS[:Format] = format
|
30
|
+
end
|
31
|
+
|
32
|
+
o.on("-l latest", String, "Set the name for the latest rotation symlink.") do |latest|
|
33
|
+
OPTIONS[:Latest] = latest
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on("-d destination", String, "Set the directory to move rotated backups.") do |destination|
|
37
|
+
OPTIONS[:Destination] = destination
|
38
|
+
end
|
39
|
+
|
40
|
+
o.on_tail("--copy", "Display copyright information") do
|
41
|
+
puts "#{script_name}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
|
42
|
+
puts "See http://www.oriontransfer.co.nz/ for more information."
|
43
|
+
|
44
|
+
exit
|
45
|
+
end
|
46
|
+
|
47
|
+
o.on_tail("-h", "--help", "Show this help message.") do
|
48
|
+
puts o
|
49
|
+
exit
|
50
|
+
end
|
51
51
|
end.parse!
|
52
52
|
|
53
53
|
time = Time.now
|
54
54
|
|
55
55
|
if OPTIONS[:UseUTC]
|
56
|
-
|
56
|
+
time = time.utc
|
57
57
|
end
|
58
58
|
|
59
|
-
dir = Pathname.new(ARGV[0] || ".inprogress")
|
59
|
+
dir = Pathname.new(ARGV[0] || "backup.inprogress")
|
60
60
|
rotation_dir = Pathname.new(OPTIONS[:Destination] ? OPTIONS[:Destination] : dir.dirname)
|
61
61
|
rotated_name = time.strftime(OPTIONS[:Format])
|
62
62
|
rotated_dir = rotation_dir + rotated_name
|
63
63
|
latest_link = rotation_dir + OPTIONS[:Latest]
|
64
64
|
|
65
|
+
unless File.exist? dir
|
66
|
+
$stderr.puts "Can not find source directory to rotate: #{dir} in #{Dir.getwd}!"
|
67
|
+
exit(10)
|
68
|
+
end
|
69
|
+
|
70
|
+
if File.exist? rotated_dir
|
71
|
+
$stderr.puts "Destination rotation name #{rotated_dir} already exists in #{Dir.getwd}!"
|
72
|
+
exit(20)
|
73
|
+
end
|
74
|
+
|
75
|
+
puts "Rotating #{dir} to #{rotated_dir}..."
|
76
|
+
|
65
77
|
# Move rotated dir
|
66
78
|
FileUtils.mv(dir, rotated_dir)
|
67
79
|
|
68
80
|
# Recreate latest symlink
|
69
81
|
if File.symlink?(latest_link)
|
70
|
-
|
71
|
-
|
82
|
+
puts "Removing old latest link..."
|
83
|
+
FileUtils.rm(latest_link)
|
72
84
|
end
|
73
85
|
|
74
86
|
puts "Creating latest symlink to #{rotated_name}"
|