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,75 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script takes a given path, and renames it with the given format.
4
+ # It then ensures that there is a symlink called "latest" that points
5
+ # to the renamed directory.
6
+
7
+ require 'pathname'
8
+ require 'fileutils'
9
+ require 'optparse'
10
+
11
+ OPTIONS = {
12
+ :Format => "%Y.%m.%d-%H.%M.%S",
13
+ :Latest => "latest",
14
+ :UseGMT => true,
15
+ :Destination => nil
16
+ }
17
+
18
+ ARGV.options do |o|
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
+ end.parse!
52
+
53
+ time = Time.now
54
+
55
+ if OPTIONS[:UseUTC]
56
+ time = time.utc
57
+ end
58
+
59
+ dir = Pathname.new(ARGV[0] || ".inprogress")
60
+ rotation_dir = Pathname.new(OPTIONS[:Destination] ? OPTIONS[:Destination] : dir.dirname)
61
+ rotated_name = time.strftime(OPTIONS[:Format])
62
+ rotated_dir = rotation_dir + rotated_name
63
+ latest_link = rotation_dir + OPTIONS[:Latest]
64
+
65
+ # Move rotated dir
66
+ FileUtils.mv(dir, rotated_dir)
67
+
68
+ # Recreate latest symlink
69
+ if File.symlink?(latest_link)
70
+ puts "Removing old latest link..."
71
+ FileUtils.rm(latest_link)
72
+ end
73
+
74
+ puts "Creating latest symlink to #{rotated_name}"
75
+ FileUtils.ln_s(rotated_name, latest_link)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Commands = {
4
+ "mount" => "mount",
5
+ "unmount" => "umount"
6
+ }
7
+
8
+ DevicePaths = [
9
+ "/dev/disk/by-label",
10
+ "/dev/disk/by-uuid",
11
+ "/dev"
12
+ ]
13
+
14
+ action = ARGV[0]
15
+ disk_name = ARGV[1]
16
+
17
+ mountpoint = File.join('', 'mnt', disk_name)
18
+
19
+ if (action == 'mountpoint')
20
+ puts File.join(mountpoint, ARGV[2..-1])
21
+ else
22
+ puts "#{action.capitalize}ing #{mountpoint}..."
23
+ system Commands[action], mountpoint
24
+
25
+ if $?.exitstatus != 0 or $?.exitstatus != 3383
26
+ exit 5
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'base64'
4
+ cmd = Base64.decode64(ARGV[0])
5
+ exec cmd
6
+
@@ -0,0 +1,33 @@
1
+
2
+ module LSync
3
+
4
+ class BackupError < StandardError
5
+ def initialize(reason, components = {})
6
+ @reason = reason
7
+ @components = components
8
+ end
9
+
10
+ def to_s
11
+ @reason
12
+ end
13
+
14
+ attr :reason
15
+ attr :components
16
+ end
17
+
18
+ class BackupScriptError < BackupError
19
+ end
20
+
21
+ class BackupMethodError < BackupError
22
+ end
23
+
24
+ class ConfigurationError < BackupError
25
+ end
26
+
27
+ class BackupActionError < BackupError
28
+ def initialize(server, action, exception)
29
+ super("Backup action failed: #{action} (#{exception.to_s})", :action => action, :exception => exception)
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,249 @@
1
+
2
+ # A backup plan is a rule-based engine to process individual scripts.
3
+ # Failure and success can be delt with over multiple scripts.
4
+
5
+ require 'ruleby'
6
+
7
+ module Ruleby
8
+ def self.engine(name, &block)
9
+ e = Core::Engine.new
10
+ yield e if block_given?
11
+ return e
12
+ end
13
+ end
14
+
15
+ module LSync
16
+
17
+ BuiltInCommands = {
18
+ "ping-host" => "ping -c 4 -t 5 -o"
19
+ }
20
+
21
+ module Facts
22
+ class Initial
23
+ end
24
+
25
+ class StageSucceeded
26
+ def initialize(stage)
27
+ @stage = stage
28
+ puts "Stage Succeeded: #{@stage.name}"
29
+ end
30
+
31
+ attr :stage
32
+
33
+ def name
34
+ @stage.name
35
+ end
36
+ end
37
+
38
+ class StageFailed
39
+ def initialize(stage)
40
+ @stage = stage
41
+ end
42
+
43
+ attr :stage
44
+
45
+ def name
46
+ @stage.name
47
+ end
48
+ end
49
+
50
+ class ScriptSucceeded
51
+ def initialize(stage, script)
52
+ @stage = stage
53
+ @script = script
54
+ end
55
+
56
+ attr :stage
57
+ attr :script
58
+ end
59
+
60
+ class ScriptFailed
61
+ def initialize(stage, script)
62
+ @stage = stage
63
+ @script = script
64
+ end
65
+
66
+ attr :stage
67
+ attr :script
68
+ end
69
+ end
70
+
71
+ class BackupPlanRulebook < Ruleby::Rulebook
72
+ include Facts
73
+
74
+ def rules
75
+ #rule [ScriptSucceeded, :m] do |v|
76
+ # script = v[:m].script
77
+ # puts "Backup #{script.dump} successful"
78
+ #end
79
+
80
+ rule [ScriptFailed, :m] do |v|
81
+ script = v[:m].script
82
+ puts "*** Script #{script} failed"
83
+ end
84
+
85
+ #rule [StageSucceeded, :m] do |v|
86
+ # stage = v[:m].stage
87
+ # puts "Stage #{stage.name.dump} successful"
88
+ #end
89
+ end
90
+ end
91
+
92
+ class StageRulebook < Ruleby::Rulebook
93
+ include Facts
94
+
95
+ def initialize(engine, stage)
96
+ super(engine)
97
+ @stage = stage
98
+ end
99
+
100
+ def rules
101
+ # Does this stage have any rules? (i.e. can it run in any case?)
102
+ if @stage.rules.size > 0
103
+ puts "Loading rules for stage #{@stage.name.dump}..."
104
+ @stage.rules.each do |name, r|
105
+ puts "\t#{name}..."
106
+
107
+ r["when"].each do |s|
108
+ puts "\t\t#{s.dump}"
109
+ end
110
+
111
+ options = r.dup
112
+ wh = options.delete("when")
113
+
114
+ # Build rule
115
+ rule("#{@stage.name}_#{name}".to_sym, options, *wh) do |v|
116
+ @stage.run_scripts
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ # Bring names into the right scope (i.e. Facts)
123
+ def __eval__(x)
124
+ eval(x)
125
+ end
126
+ end
127
+
128
+ class Stage
129
+ protected
130
+ RuleConfigKeys = Set.new(["priority", "when"])
131
+
132
+ def process_rules config
133
+ rules = config.keys_matching(/^rule\.(.*)$/)
134
+
135
+ if rules.size > 0
136
+ # Okay
137
+ elsif config.key? "when"
138
+ rules = {
139
+ "rule.default" => config.delete_if { |k,v| !RuleConfigKeys.include?(k) }
140
+ }
141
+ else
142
+ return {}
143
+ end
144
+
145
+ rules.keys.each do |rule_name|
146
+ options = {}
147
+ w = rules[rule_name].delete("when") || []
148
+ w = [w] if w.is_a? String
149
+
150
+ rules[rule_name].each { |k,v| options[k.to_sym] = v }
151
+ rules[rule_name] = options
152
+ rules[rule_name]["when"] = w.collect { |s| s.gsub('@', '#') }
153
+ end
154
+
155
+ rules
156
+ end
157
+
158
+ def process_scripts config
159
+ config["scripts"].collect do |s|
160
+ s.match(/^([^\s]+)(.*)$/)
161
+
162
+ if BuiltInCommands.key? $1
163
+ BuiltInCommands[$1] + $2
164
+ else
165
+ s
166
+ end
167
+ end
168
+ end
169
+
170
+ public
171
+ def initialize(plan, name, config)
172
+ @plan = plan
173
+ @name = name
174
+
175
+ @scripts = process_scripts(config)
176
+ @rules = process_rules(config)
177
+ end
178
+
179
+ def run_scripts
180
+ failed = false
181
+
182
+ puts "Running stage #{@name}..."
183
+ @scripts.each do |script|
184
+ puts "\tRunning Script #{script}..."
185
+
186
+ if system(script)
187
+ @plan.engine.assert Facts::ScriptSucceeded.new(self, script)
188
+ else
189
+ @plan.engine.assert Facts::ScriptFailed.new(self, script)
190
+ failed = true
191
+ end
192
+ end
193
+
194
+ if failed
195
+ @plan.engine.assert Facts::StageFailed.new(self)
196
+ else
197
+ @plan.engine.assert Facts::StageSucceeded.new(self)
198
+ end
199
+ end
200
+
201
+ attr :name
202
+ attr :scripts
203
+ attr :rules
204
+ end
205
+
206
+ class BackupPlan
207
+ def initialize(config, logger = nil)
208
+ @logger = logger || Logger.new(STDOUT)
209
+
210
+ @config = config.keys_matching(/^scripts\.(.*)$/)
211
+ @stages = config.keys_matching(/^stage\.(.*)$/) { |c,name| Stage.new(self, name, c) }
212
+ end
213
+
214
+ attr :logger, true
215
+ attr :config
216
+
217
+ def run_backup
218
+ Ruleby.engine :engine do |e|
219
+ @engine = e
220
+
221
+ puts " Loading Rules ".center(80, "=")
222
+
223
+ BackupPlanRulebook.new(e).rules
224
+
225
+ @stages.each do |k,s|
226
+ StageRulebook.new(e, s).rules
227
+ end
228
+
229
+ puts " Processing Rules ".center(80, "=")
230
+
231
+ e.assert Facts::Initial.new
232
+
233
+ e.match
234
+ @engine = nil
235
+ end
236
+
237
+ puts " Finished ".center(80, "=")
238
+ end
239
+
240
+ attr :engine
241
+ attr :config
242
+ attr :stages
243
+
244
+ def self.load_from_file(path)
245
+ new(YAML::load(File.read(path)))
246
+ end
247
+ end
248
+
249
+ end
@@ -0,0 +1,136 @@
1
+
2
+ # A backup script coordinates "one" backup as a unit.
3
+
4
+ require 'lsync/action'
5
+ require 'lsync/method'
6
+ require 'lsync/server'
7
+ require 'lsync/directory'
8
+
9
+ module LSync
10
+
11
+ class BackupScript
12
+ private
13
+ # Given a name, find out which server config matches it
14
+ def find_named_server name
15
+ if @servers.key? name
16
+ return @servers[name]
17
+ else
18
+ hostname = Socket.gethostbyname(name)[0] rescue name
19
+ return @servers.values.find { |s| s["host"] == hostname }
20
+ end
21
+ end
22
+
23
+ # Find out the config section for the current server
24
+ def find_current_server
25
+ server = nil
26
+
27
+ # Find out if the master server is local...
28
+ if @master.is_local?
29
+ server = @master
30
+ else
31
+ # Find a server config that specifies the local host
32
+ server = @servers.values.find { |s| s.is_local? }
33
+ end
34
+
35
+ return server
36
+ end
37
+
38
+ def script_logger
39
+ if @config["log-file"]
40
+ return Logger.new(@config["log-file"], 'weekly')
41
+ end
42
+ end
43
+
44
+ public
45
+ def initialize(config, logger = nil)
46
+ @config = config
47
+
48
+ @logger = logger || Logger.new(STDOUT)
49
+
50
+ @servers = config.keys_matching(/^server\./) { |c,n| Server.new(c) }
51
+ @directories = config.keys_matching(/^directory\./) { |c,n| Directory.new(c) }
52
+
53
+ @master = find_named_server(config["master"])
54
+
55
+ if @master == nil
56
+ raise ConfigurationError.new("Could not determine master server!", :script => self)
57
+ end
58
+
59
+ @method = Method.new(config["method"])
60
+ @log_buffer = nil
61
+ end
62
+
63
+ attr :logger, true
64
+ attr :master
65
+ attr :method
66
+ attr :servers
67
+ attr :directories
68
+ attr :log_buffer
69
+
70
+ def run_backup
71
+ # We buffer the log data so that if there is an error it is available to the notification sub-system
72
+ @log_buffer = StringIO.new
73
+ logger = @logger.tee(script_logger, Logger.new(@log_buffer))
74
+
75
+ current = find_current_server
76
+
77
+ # At this point we must know the current server or we can't continue
78
+ if current == nil
79
+ raise BackupScriptError.new("Could not determine current server!", :script => self, :master => @master)
80
+ end
81
+
82
+ if @master.is_local?
83
+ logger.info "We are the master server..."
84
+ else
85
+ logger.info "We are not the master server..."
86
+ logger.info "Master server is #{@master}..."
87
+ end
88
+
89
+ # Run server pre-scripts.. if these fail then we abort the whole backup
90
+ begin
91
+ @method.run_actions(:before, logger)
92
+ @master.run_actions(:before, logger)
93
+ rescue AbortBackupException
94
+ return
95
+ end
96
+
97
+ logger.info "Running backups for server #{current}..."
98
+
99
+ @servers.each do |name, s|
100
+ # S is always a data destination, therefore s can't be @master
101
+ next if s == @master
102
+
103
+ # Skip servers that shouldn't be processed
104
+ unless @method.should_run?(@master, current, s)
105
+ logger.info "\t" + "Skipping".rjust(20) + " : #{s}"
106
+ next
107
+ end
108
+
109
+ # Run pre-scripts for a particular server
110
+ begin
111
+ s.run_actions(:before, logger)
112
+ rescue AbortBackupException
113
+ next
114
+ end
115
+
116
+ @directories.each do |name, d|
117
+ logger.info "\t" + ("Processing " + d.to_s).rjust(20) + " : #{s}"
118
+
119
+ @method.logger = logger
120
+ @method.run(@master, s, d)
121
+ end
122
+
123
+ # Run post-scripts for a particular server
124
+ s.run_actions(:after, logger)
125
+ end
126
+
127
+ @method.run_actions(:after, logger)
128
+ @master.run_actions(:after, logger)
129
+ end
130
+
131
+ def self.load_from_file(path)
132
+ new(YAML::load(File.read(path)))
133
+ end
134
+ end
135
+
136
+ end