lsync 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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