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,152 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ftools'
4
+ require 'lsync'
5
+ require 'lsync/version'
6
+ require 'optparse'
7
+
8
+ OPTIONS = {
9
+ :ConfigFiles => [],
10
+ :Plan => nil,
11
+ :LogPath => "/var/log/lsync/backup.log"
12
+ }
13
+
14
+ ARGV.options do |o|
15
+ script_name = File.basename($0)
16
+
17
+ o.set_summary_indent(' ')
18
+ o.banner = "Usage: #{script_name} [options] [directory | config]"
19
+ o.define_head "This script is used to run backups. If you specify a directory, files ending in .conf will all be processed."
20
+
21
+ o.separator ""
22
+ o.separator "Help and Copyright information"
23
+
24
+ o.on("-p plan", String, "Run the specified backup plan") { |plan| OPTIONS[:Plan] = plan }
25
+ o.on("-l log_path", String, "Set the directory for backup logs") { |log_path| OPTIONS[:LogPath] = log_path }
26
+
27
+ o.on_tail("--copy", "Display copyright information") do
28
+ puts "#{script_name} v#{LSync::VERSION::STRING}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
29
+ puts "See http://www.oriontransfer.co.nz/ for more information."
30
+
31
+ exit
32
+ end
33
+
34
+ o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
35
+ end.parse!
36
+
37
+ # ============================== Main Scripts / Plans ==============================
38
+
39
+ class LogPathChecker
40
+ def initialize(directory, filename)
41
+ full_path = File.join(directory, filename)
42
+
43
+ @directory_writable = File.directory?(directory) && File.writable?(directory)
44
+ @file_not_writable = File.exist?(full_path) && !File.writable?(full_path)
45
+
46
+ @accessable = @directory_writable && !@file_not_writable
47
+ @full_path = full_path
48
+ end
49
+
50
+ attr :accessable
51
+ attr :full_path
52
+ end
53
+
54
+ def setup_logging
55
+ # Console log output
56
+ $console_log = Logger.new($stdout)
57
+ $console_log.formatter = MinimalLogFormat.new
58
+
59
+ # File log output
60
+ filename = File.basename(OPTIONS[:LogPath])
61
+ directory = File.dirname(OPTIONS[:LogPath])
62
+
63
+ if filename == nil || filename == ""
64
+ filename = "backup.log"
65
+ end
66
+
67
+ check = LogPathChecker.new(directory, filename)
68
+ unless check.accessable
69
+ $console_log.warn "Could not write to #{check.full_path.dump} - changing log path to #{Dir.pwd.dump}"
70
+
71
+ directory = Dir.pwd
72
+ check = LogPathChecker.new(directory, filename)
73
+ end
74
+
75
+ if check.accessable
76
+ $backup_log = Logger.new(check.full_path, 'weekly')
77
+ else
78
+ $console_log.warn "Could not write to #{check.full_path.dump} - not logging to disk"
79
+ $backup_log = nil
80
+ end
81
+
82
+ $logger = TeeLogger.new($console_log, $backup_log)
83
+ end
84
+
85
+ def process_config_files
86
+ ARGV.each do |p|
87
+ unless File.exists? p
88
+ $logger.error "Could not process #{p}"
89
+ exit(20)
90
+ end
91
+
92
+ if File.directory? p
93
+ OPTIONS[:ConfigFiles] += Dir[File.join(p, "*.conf")]
94
+ else
95
+ OPTIONS[:ConfigFiles] << p
96
+ end
97
+ end
98
+ end
99
+
100
+ def validate_options
101
+ if OPTIONS[:Plan] && OPTIONS[:ConfigFiles].size > 0
102
+ $logger.error "Please specify either a backup plan or a set of backup scripts, but not both in one run."
103
+ exit(27)
104
+ end
105
+ end
106
+
107
+ def run_backups
108
+ OPTIONS[:ConfigFiles].each do |c|
109
+ config = LSync::BackupScript.load_from_file(c)
110
+ config.logger = $logger
111
+ config.run_backup
112
+ end
113
+
114
+ if OPTIONS[:Plan]
115
+ config = LSync::BackupPlan.load_from_file(OPTIONS[:Plan])
116
+ config.logger = $logger
117
+ config.run_backup
118
+ end
119
+ end
120
+
121
+ def dump_exception_backtrace ex
122
+ ex.backtrace.each do |bt|
123
+ $logger.error bt
124
+ end
125
+ end
126
+
127
+ setup_logging
128
+
129
+ process_config_files
130
+
131
+ validate_options
132
+
133
+ $logger.info " Backup beginning at #{Time.now.to_s} ".center(96, "=")
134
+
135
+ begin
136
+ run_backups
137
+ rescue LSync::BackupError
138
+ $logger.error "#{$!.class.name}: #{$!.to_s}. Dumping backtrace:"
139
+ dump_exception_backtrace($!)
140
+ exit(230)
141
+ rescue Interrupt
142
+ $logger.error "Backup process recevied interrupt (Ctrl-C). Dumping backtrace:"
143
+ dump_exception_backtrace($!)
144
+ exit(240)
145
+ rescue
146
+ $logger.error "Unknown exception #{$!.class.name} thrown: #{$!.to_s}. Dumping backtrace:"
147
+ dump_exception_backtrace($!)
148
+ exit(250)
149
+ ensure
150
+ $logger.info " Backup finishing at #{Time.now.to_s} ".center(96, "=")
151
+ end
152
+
@@ -0,0 +1,40 @@
1
+ # Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv2.
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ require 'rubygems'
17
+
18
+ gem 'termios'
19
+ gem 'net-ssh'
20
+ gem 'ruleby'
21
+
22
+ require 'yaml'
23
+ require 'socket'
24
+ require 'set'
25
+ require 'logger'
26
+
27
+ require 'lsync/version'
28
+ require 'lsync/extensions'
29
+ require 'lsync/backup_script'
30
+ require 'lsync/backup_plan'
31
+ require 'lsync/tee_logger'
32
+
33
+ require 'fileutils'
34
+ require 'optparse'
35
+
36
+ require 'open-uri'
37
+
38
+ module LSync
39
+
40
+ end
@@ -0,0 +1,100 @@
1
+
2
+ require 'pathname'
3
+ require 'lsync/run'
4
+ require 'lsync/backup_error'
5
+
6
+ module LSync
7
+
8
+ class AbortBackupException < Exception
9
+
10
+ end
11
+
12
+ class Action
13
+ def initialize(function)
14
+ @function = function
15
+
16
+ if @function.match(/^\%([a-z]+)(\s+.*)?$/)
17
+ @script_name = $1
18
+ @arguments = $2
19
+ else
20
+ @script_name = nil
21
+ end
22
+ end
23
+
24
+ def to_s
25
+ @function
26
+ end
27
+
28
+ def run_on_server(server, logger)
29
+ logger.info "Running #{@function} on #{server}"
30
+
31
+ if server.is_local?
32
+ run_locally(server, logger)
33
+ else
34
+ run_remotely(server, logger)
35
+ end
36
+ end
37
+
38
+ private
39
+ def run_locally(server, logger)
40
+ command = nil
41
+
42
+ if @script_name
43
+ uname = `uname`.chomp.downcase
44
+
45
+ local_path = Action.script_path(uname, @script_name)
46
+ command = local_path.to_cmd + @arguments
47
+ else
48
+ command = @function
49
+ end
50
+
51
+ ret = nil
52
+ Dir.chdir(server.root_path) do
53
+ ret = LSync.run_command(command, logger)
54
+ end
55
+
56
+ case(ret)
57
+ when 0
58
+ return
59
+ when 1
60
+ raise AbortBackupException
61
+ else
62
+ raise BackupActionError
63
+ end
64
+ end
65
+
66
+ def run_remotely(server, logger)
67
+ conn = server.connect
68
+ conn.send_object([:set_working_dir, server.root_path])
69
+
70
+ if @script_name
71
+ uname = `uname`.chomp.downcase
72
+
73
+ local_path = Action.script_path(uname, @script_name)
74
+
75
+ logger.info("Sending run_script #{@script_name}...")
76
+ conn.send_object([:run_script, @script_name, Pathname.new(local_path).read, @arguments])
77
+ else
78
+ logger.info("Sending run_command #{@function}...")
79
+ conn.send_object([:run_command, @function])
80
+ end
81
+
82
+ conn.run do |message|
83
+ break if message == :done
84
+
85
+ logger.send(*message)
86
+ end
87
+ end
88
+
89
+ def self.script_path(platform, name)
90
+ exact_script_path(platform, name) || exact_script_path("generic", name)
91
+ end
92
+
93
+ private
94
+ def self.exact_script_path(platform, name)
95
+ path = (Pathname.new(__FILE__).dirname + "actions" + platform + name).expand_path
96
+ path.exist? ? path : nil
97
+ end
98
+ end
99
+
100
+ end
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ DISKUTIL = "diskutil"
4
+ action = ARGV[0]
5
+ disk_name = ARGV[1]
6
+
7
+ def get_disk_id(name)
8
+ begin
9
+ `diskutil list`.match(/#{name}\s*\*?[0-9]+\.[0-9]+ .B\s+(disk[0-9]s[0-9])$/)[1]
10
+ rescue
11
+ exit 5
12
+ end
13
+ end
14
+
15
+ if (action == 'mountpoint')
16
+ puts File.join('', 'Volumes', disk_name, ARGV[2..-1])
17
+ else
18
+ system DISKUTIL, action, get_disk_id(disk_name)
19
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'base64'
4
+
5
+ cmd = Base64.decode64(ARGV[0])
6
+
7
+ exec cmd
8
+
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script takes a specified directory, and removes any directories that
4
+ # don't match the supplied policy. Thanks to Scott Lu and his "snapfilter"
5
+ # command which made me realise how complicated my first attempt at doing this
6
+ # was.
7
+
8
+ require 'pathname'
9
+ require 'fileutils'
10
+ require 'optparse'
11
+
12
+ require 'set'
13
+ require 'time'
14
+ # Required for strptime
15
+ require 'date'
16
+
17
+ class Rotation
18
+ def initialize(path, time)
19
+ @path = path
20
+ @time = time
21
+ end
22
+
23
+ attr :time
24
+ attr :path
25
+
26
+ # Sort in reverse order by default
27
+ def <=> other
28
+ return other.time <=> @time
29
+ end
30
+ end
31
+
32
+ class Period
33
+ KeepOldest = Proc.new do |t1, t2|
34
+ t1 > t2
35
+ end
36
+
37
+ KeepYoungest = Proc.new do |t1, t2|
38
+ t1 < t2
39
+ end
40
+
41
+ def initialize(count)
42
+ @count = count
43
+ end
44
+
45
+ def filter(values, options = {})
46
+ slots = {}
47
+
48
+ keep = (options[:keep] == :youngest) ? KeepYoungest : KeepOldest
49
+
50
+ values.each do |value|
51
+ time = value.time
52
+
53
+ k = key(time)
54
+
55
+ # We want to keep the newest backup if possible (<).
56
+ next if slots.key?(k) and keep.call(value.time, slots[k].time)
57
+
58
+ slots[k] = value
59
+ end
60
+
61
+ sorted_values = slots.values.sort
62
+
63
+ return sorted_values[0...@count]
64
+ end
65
+
66
+ def key(t)
67
+ raise ArgumentError
68
+ end
69
+
70
+ def mktime(year, month=1, day=1, hour=0, minute=0, second=0)
71
+ return Time.gm(year, month, day, hour, minute, second)
72
+ end
73
+
74
+ attr :count
75
+ end
76
+
77
+ class Hourly < Period
78
+ def key(t)
79
+ mktime(t.year, t.month, t.day, t.hour)
80
+ end
81
+ end
82
+
83
+ class Daily < Period
84
+ def key(t)
85
+ mktime(t.year, t.month, t.day)
86
+ end
87
+ end
88
+
89
+ class Weekly < Period
90
+ def key(t)
91
+ mktime(t.year, t.month, t.day) - (t.wday * 3600 * 24)
92
+ end
93
+ end
94
+
95
+ class Monthly < Period
96
+ def key(t)
97
+ mktime(t.year, t.month)
98
+ end
99
+ end
100
+
101
+ class Quarterly < Period
102
+ def key(t)
103
+ mktime(t.year, (t.month - 1) / 3 * 3 + 1)
104
+ end
105
+ end
106
+
107
+ class Yearly < Period
108
+ def key(t)
109
+ mktime(t.year)
110
+ end
111
+ end
112
+
113
+ class Policy
114
+ def initialize
115
+ @periods = {}
116
+ end
117
+
118
+ def <<(period)
119
+ @periods[period.class] = period
120
+ end
121
+
122
+ def filter(values, options = {})
123
+ filtered_values = Set.new
124
+ @periods.values.each do |period|
125
+ filtered_values += period.filter(values, options)
126
+ end
127
+
128
+ return filtered_values.to_a, (Set.new(values) - filtered_values).to_a
129
+ end
130
+
131
+ attr :periods
132
+ end
133
+
134
+ OPTIONS = {
135
+ :Format => "%Y.%m.%d-%H.%M.%S",
136
+ :Destination => "./*",
137
+ :Policy => Policy.new,
138
+ :PolicyOptions => {},
139
+ :Wet => true
140
+ }
141
+
142
+ ARGV.options do |o|
143
+ script_name = File.basename($0)
144
+
145
+ o.set_summary_indent(' ')
146
+ o.banner = "Usage: #{script_name} [options] [directory]"
147
+ o.define_head "This script is used to prune old backups."
148
+
149
+ o.separator ""
150
+ o.separator "Help and Copyright information"
151
+
152
+ o.on("-f format", String, "Set the format of the rotated directory names. See Time$strftime") do |format|
153
+ OPTIONS[:Format] = format
154
+ end
155
+
156
+ o.on("-d destination", String, "Set the directory that contains backups to prune.") do |destination|
157
+ OPTIONS[:Destination] = destination
158
+ end
159
+
160
+ o.on("--dry-run", "Print out what would be deleted, but don't actually delete anything.") do
161
+ OPTIONS[:Wet] = false
162
+ end
163
+
164
+ o.separator ""
165
+
166
+ o.on("--default-policy", "Sets up a typical policy retaining a reasonable number of rotations for up to 20 years.") do
167
+ OPTIONS[:Policy] << Hourly.new(24)
168
+ OPTIONS[:Policy] << Daily.new(7*4)
169
+ OPTIONS[:Policy] << Weekly.new(52)
170
+ OPTIONS[:Policy] << Monthly.new(12*3)
171
+ OPTIONS[:Policy] << Quarterly.new(4*10)
172
+ OPTIONS[:Policy] << Yearly.new(20)
173
+ end
174
+
175
+ o.separator ""
176
+
177
+ o.on("--keep-oldest", "Keep older backups within the same period divsion (the default).") do
178
+ OPTIONS[:PolicyOptions][:keep] = :oldest
179
+ end
180
+
181
+ o.on("--keep-youngest", "Keep younger backups within the same period division.") do
182
+ OPTIONS[:PolicyOptions][:keep] = :youngest
183
+ end
184
+
185
+ o.on("--hourly count", Integer, "Set the number of hourly backups to keep.") do |count|
186
+ OPTIONS[:Policy] << Hourly.new(count)
187
+ end
188
+
189
+ o.on("--daily count", Integer, "Set the number of daily backups to keep.") do |count|
190
+ OPTIONS[:Policy] << Daily.new(count)
191
+ end
192
+
193
+ o.on("--weekly count", Integer, "Set the number of weekly backups to keep.") do |count|
194
+ OPTIONS[:Policy] << Weekly.new(count)
195
+ end
196
+
197
+ o.on("--monthly count", Integer, "Set the number of monthly backups to keep.") do |count|
198
+ OPTIONS[:Policy] << Monthly.new(count)
199
+ end
200
+
201
+ o.on("--quaterly count", Integer, "Set the number of monthly backups to keep.") do |count|
202
+ OPTIONS[:Policy] << Quarterly.new(count)
203
+ end
204
+
205
+ o.on("--yearly count", Integer, "Set the number of monthly backups to keep.") do |count|
206
+ OPTIONS[:Policy] << Yearly.new(count)
207
+ end
208
+
209
+ o.separator ""
210
+
211
+ o.on_tail("--copy", "Display copyright information") do
212
+ puts "#{script_name}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
213
+ puts "See http://www.oriontransfer.co.nz/ for more information."
214
+
215
+ exit
216
+ end
217
+
218
+ o.on_tail("-h", "--help", "Show this help message.") do
219
+ puts o
220
+ exit
221
+ end
222
+ end.parse!
223
+
224
+ backups = []
225
+
226
+ Dir[OPTIONS[:Destination]].each do |path|
227
+ next if path.match("latest")
228
+ date_string = File.basename(path)
229
+
230
+ begin
231
+ backups << Rotation.new(path, DateTime.strptime(date_string, OPTIONS[:Format]))
232
+ rescue ArgumentError
233
+ puts "Skipping #{path}, error parsing #{date_string}: #{$!}"
234
+ end
235
+ end
236
+
237
+ keep, erase = OPTIONS[:Policy].filter(backups)
238
+
239
+ if OPTIONS[:Wet]
240
+ erase.sort.each do |backup|
241
+ puts "Erasing #{backup.path}..."
242
+ $stdout.flush
243
+ FileUtils.rm_rf(backup.path)
244
+ end
245
+ else
246
+ puts "*** Dry Run ***"
247
+ puts "\tKeeping:"
248
+ keep.sort.each { |backup| puts "\t\t#{backup.path}" }
249
+ puts "\tErasing:"
250
+ erase.sort.each { |backup| puts "\t\t#{backup.path}" }
251
+ end