bacuwatch 1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ The Bacuwatch program can be used to keep a watch on a group of Bacula
2
+ jobs. Typically, it is run periodically from a cron job, and is
3
+ generally configured to send an email message with a single line
4
+ status report on each backup job to the Bacula administrator and
5
+ optionally to send a more detailed report on each job to the user of
6
+ the machine which that job backs up.
7
+
8
+ A typical summary report would consist of a series of single-line
9
+ reports, such as:
10
+
11
+ Okay for delta-force; (Full: 24 days; Last: 5 hours)
12
+
13
+ A typical job report contains the same information, but additionally
14
+ (and at no extra charge) shows the storage volume and number of files
15
+ backed up, for example:
16
+
17
+ Hello Most Honorable Sensai Norris,
18
+
19
+ This is the Bacula Watcher program, reporting on the state of
20
+ the backup job, delta-force, for your computer.
21
+
22
+ - The last full backup ran 24 days ago,
23
+ on Saturday, October 21, 2006.
24
+ It contained 5 gigabytes of data in 172,198 files.
25
+
26
+ - The last incremental backup ran 5 hours ago,
27
+ on Tuesday, November 14, 2006.
28
+ It contained 28 megabytes of data in 81 files.
29
+
30
+ All seems well with the 'delta-force' backup job.
31
+
32
+ When run with no options, the bacuwatch program prints out a job
33
+ summary. To have it periodically email summary and status reports,
34
+ for example at 10:00 every Tuesday morning, the crontab entry would
35
+ be:
36
+
37
+ 0 10 * * 2 cd $HOME/bacuview/bacuwatch && ./bacuwatch -summary -notify
@@ -0,0 +1,214 @@
1
+ #! /usr/bin/ruby -sw
2
+
3
+ #require "rubygems"
4
+ #require "pp"
5
+ require "mysql"
6
+ require "postgres"
7
+ require "time"
8
+
9
+ #
10
+ # Define the configuration language, and read in the configuration.
11
+ #
12
+
13
+ @db_args = {
14
+ "dbms" => "postgres", "host" => "localhost",
15
+ "database" => "bacula", "user" => "bacuview", "password" => "bacuview"
16
+ }
17
+ def db(*args)
18
+ @db_args.merge!(args[0])
19
+ end
20
+
21
+ @mail_suffix = nil
22
+ def mailhost(hostname)
23
+ @mail_suffix = hostname
24
+ end
25
+
26
+ def mail_fixup(addr)
27
+ return nil unless addr
28
+ addr.gsub(/@$/, "@#{@mail_suffix}")
29
+ end
30
+
31
+ @summaries_to = []
32
+ def summary(*addrs)
33
+ @summaries_to += addrs.map{ |addr| mail_fixup(addr) }
34
+ end
35
+
36
+ @watched = []
37
+ def watch(job, addr=nil, name=nil)
38
+ @watched << [ job, mail_fixup(addr), name ]
39
+ end
40
+
41
+ config_path =
42
+ [
43
+ defined?($config) && $config,
44
+ File.join(File.dirname(__FILE__), 'bacuwatch.conf'),
45
+ File.join(ENV["HOME"], '.bacuwatch.conf'),
46
+ '/etc/bacuwatch.conf'
47
+ ].compact
48
+ config = config_path.find{|fn| File.exist? fn}
49
+ if not config
50
+ puts [ "No config file found among:", config_path ].join("\n ")
51
+ exit
52
+ end
53
+ load config
54
+
55
+ class Numeric
56
+ def days
57
+ self * 86400
58
+ end
59
+
60
+ def ago
61
+ d = self
62
+ return "never" if d <= 0
63
+ return (d).to_s + " seconds" if d < 2*60
64
+ return (d/60).to_s + " minutes" if d < 2*60*60
65
+ return (d/(60*60)).to_s + " hours" if d < 2*60*60*24
66
+ return (d/(60*60*24)).to_s + " days" if d < 2*60*60*24*30
67
+ return (d/(60*60*24*30)).to_s + " months" if d < 2*60*60*24*365
68
+ return (d/(60*60*24*365)).to_s + " years"
69
+ end
70
+ end
71
+
72
+ class String
73
+ def commatize
74
+ return self if size < 5
75
+ self.reverse.gsub(/(\d{3}\B)/, '\1,').reverse
76
+ end
77
+
78
+ def suffixize
79
+ n = self.to_i
80
+ return self + " " if n < 1000*2
81
+ return (n/1000).to_s + " kilo" if n < 1000*1000*2
82
+ return (n/(1000*1000)).to_s + " mega" if n < 1000*1000*1000*2
83
+ return (n/(1000*1000*1000)).to_s + " giga" if n < 1000*1000*1000*1000*2
84
+ return (n/(1000*1000*1000*1000)).to_s + " tera"
85
+ end
86
+ end
87
+
88
+ class Watcher
89
+ attr :stat
90
+ attr :report
91
+
92
+ @@summary_msg = []
93
+ def self.summary
94
+ @@summary_msg.map{ |s| s.gsub!(/^(\S)/, ' \1') }
95
+ @@summary_msg.sort!
96
+ @@summary_msg
97
+ end
98
+
99
+ def initialize(db, dbms, job, name)
100
+ @db = db
101
+ @dbms = dbms
102
+ @report = []
103
+
104
+ report << "Hello" + (name ? " #{name}" : "") + ","
105
+ report << ""
106
+ report << "This is the Bacula Watcher program, reporting on the state of"
107
+ report << "the backup job, #{job}, for your computer."
108
+ report << ""
109
+ full = summarize(job, "F")
110
+ incr = summarize(job, "I")
111
+ status(job, full, incr)
112
+ report.join("\n")
113
+ end
114
+
115
+ def query_job(job, level=nil)
116
+ query = "select starttime, jobfiles, jobbytes, level " +
117
+ "from Job where type='B' and jobstatus='T' and " +
118
+ "name='#{job}' and level='#{level}' order by starttime desc limit 1;"
119
+ if @dbms == "mysql"
120
+ @db.query(query).fetch_row
121
+ else
122
+ @db.exec(query).result[0]
123
+ end
124
+ end
125
+
126
+ def summarize(job, level)
127
+ l = { "I" => "incremental", "F" => "full" }[level] || level
128
+ r = query_job(job, level)
129
+ if r == nil
130
+ report << " - There has never been a #{l} backup performed."
131
+ d = nil
132
+ else
133
+ t = Time.parse(r[0])
134
+ d = (Time.now - t).to_i
135
+ report <<
136
+ " - The last #{l} backup ran #{d.ago} ago,\n" +
137
+ " on #{t.strftime('%A, %B %d, %Y')}.\n" +
138
+ " It contained #{r[2].suffixize}bytes of data " +
139
+ "in #{r[1].commatize} files."
140
+ end
141
+ report << ""
142
+ return d
143
+ end
144
+
145
+ def status(job, full, incr)
146
+ last = incr ? (full && full < incr ? full : incr) : full
147
+ err = []
148
+ if !full
149
+ err << " - The #{job} backup job has never run successfully."
150
+ else
151
+ err << " - The last full backup is too old." if full > 45.days
152
+ err << " - The most recent backup is too old." if last > 15.days
153
+ end
154
+
155
+ if err.empty?
156
+ @stat = "Okay"
157
+ report << "All seems well with the '#{job}' backup job."
158
+ else
159
+ @stat = "ERROR"
160
+ report << "There seems to be a PROBLEM with the '#{job}' backup job."
161
+ [ "", err, "" ].flatten.each do |e| report << e end
162
+ end
163
+ @@summary_msg << "#{stat} for #{job}; " +
164
+ "(Full: #{full.to_i.ago}; Last: #{last.to_i.ago})"
165
+ end
166
+ end
167
+
168
+ def mail_to(addr, subject, msg)
169
+ mail = open("|mail -s '#{subject}' #{addr}", "w")
170
+ mail.puts msg
171
+ mail.close
172
+ end
173
+
174
+ if defined?($help) and $help
175
+ print <<-EOF
176
+ bacuwatch [-help] [-quiet] [-summary] [-notify] [-config <config-file>]
177
+
178
+ -help: Prints this help information.
179
+ -quiet: Disables printing the summary report to standard output.
180
+ -config: Specifies a configuration file.
181
+ -summary: Emails a summary report to each of the addresses
182
+ listed in a "summary" statement.
183
+ -notify: Emails a job report to the email address
184
+ listed in the "watch" statement for that job.
185
+ EOF
186
+ exit
187
+ end
188
+
189
+ if @db_args["dbms"] == "mysql"
190
+ db = Mysql.new(@db_args["host"],
191
+ @db_args["user"], @db_args["password"], @db_args["database"])
192
+ else
193
+ db = PGconn.new(@db_args["host"], 5432, "", "", @db_args["database"],
194
+ @db_args["user"], @db_args["password"])
195
+ end
196
+ @watched.each { |watch_info|
197
+ job, addr, name = watch_info
198
+ w = Watcher.new(db, @db_args["dbms"], job, name)
199
+ subject = "Bacula Watcher: #{w.stat} for #{job}."
200
+ if addr and defined?($notify) and $notify
201
+ mail_to(addr, subject, w.report)
202
+ end
203
+ }
204
+ db.close
205
+
206
+ if defined?($summary) and $summary
207
+ @summaries_to.each { |addr|
208
+ mail_to(addr, "Bacula Watch Summary Report", Watcher::summary)
209
+ }
210
+ end
211
+
212
+ if !defined?($quiet) or !$quiet
213
+ puts Watcher::summary
214
+ end
@@ -0,0 +1,53 @@
1
+ #
2
+ # bacuwatch.conf -- the configuration file for the bacuwatch program.
3
+ #
4
+ #----
5
+ #
6
+ # A 'db' statement is required to define the connection to the bacula
7
+ # database. Only those parameters that differ from the default values
8
+ # below need to be supplied, so a statement along the lines of:
9
+ #
10
+ # db "host" => "db-server", "password" => "abracadabara"
11
+ #
12
+ # will likely suffice. The possible arguments and their defaults are:
13
+ #
14
+ # "dbms" => "postgres", "host" => "localhost",
15
+ # "database" => "bacula", "user" => "bacuview", "password" => "bacuview"
16
+ #
17
+ #
18
+ #----
19
+ #
20
+ # A 'mailhost' statement can be used to abbreviate email addresses in a
21
+ # common domain. Email addresses ending in an at-sign (@) will be sent
22
+ # to the domain listed in the mailhost statement, so a mailhost
23
+ # statement of:
24
+ #
25
+ # mailhost "example.org"
26
+ #
27
+ # will cause "carol@" in a 'watch' or 'summary' statement to be
28
+ # expanded to "carol@example.org", while email addresses not ending in
29
+ # an at-sign are used as is. This statement goes into effect when
30
+ # seen, so must preceed its first use, but can be changed in mid-file.
31
+ #
32
+ #----
33
+ #
34
+ # A 'watch' statement is used to place a watch on a particular job.
35
+ # The watch statement takes three parameters: a mandatory job name,
36
+ # an optional email address, and an optional name or title. So, watch
37
+ # statements of:
38
+ #
39
+ # watch "delta-force"
40
+ # watch "delta-force", "chuck@norris.org"
41
+ # watch "delta-force", "chuck@norris.org", "Most Honorable Sensai Norris"
42
+ #
43
+ # would watch the 'delta-force' backup job; watch the 'delta-force'
44
+ # backup job and send a report to chuck@norris.org; and watch the
45
+ # 'delta-force' backup job, sending a report to chuck@norris.org using
46
+ # the properly honorific title.
47
+ #
48
+ #----
49
+ #
50
+ # A 'summary' statement is used to email a single-line summary of the
51
+ # status of each job to each of the addresses supplied, for example:
52
+ #
53
+ # summary "alice@example.org", "bob@example.net"
data/bin/bacuwatch ADDED
@@ -0,0 +1,214 @@
1
+ #! /usr/bin/ruby -sw
2
+
3
+ #require "rubygems"
4
+ #require "pp"
5
+ require "mysql"
6
+ require "postgres"
7
+ require "time"
8
+
9
+ #
10
+ # Define the configuration language, and read in the configuration.
11
+ #
12
+
13
+ @db_args = {
14
+ "dbms" => "postgres", "host" => "localhost",
15
+ "database" => "bacula", "user" => "bacuview", "password" => "bacuview"
16
+ }
17
+ def db(*args)
18
+ @db_args.merge!(args[0])
19
+ end
20
+
21
+ @mail_suffix = nil
22
+ def mailhost(hostname)
23
+ @mail_suffix = hostname
24
+ end
25
+
26
+ def mail_fixup(addr)
27
+ return nil unless addr
28
+ addr.gsub(/@$/, "@#{@mail_suffix}")
29
+ end
30
+
31
+ @summaries_to = []
32
+ def summary(*addrs)
33
+ @summaries_to += addrs.map{ |addr| mail_fixup(addr) }
34
+ end
35
+
36
+ @watched = []
37
+ def watch(job, addr=nil, name=nil)
38
+ @watched << [ job, mail_fixup(addr), name ]
39
+ end
40
+
41
+ config_path =
42
+ [
43
+ defined?($config) && $config,
44
+ File.join(File.dirname(__FILE__), 'bacuwatch.conf'),
45
+ File.join(ENV["HOME"], '.bacuwatch.conf'),
46
+ '/etc/bacuwatch.conf'
47
+ ].compact
48
+ config = config_path.find{|fn| File.exist? fn}
49
+ if not config
50
+ puts [ "No config file found among:", config_path ].join("\n ")
51
+ exit
52
+ end
53
+ load config
54
+
55
+ class Numeric
56
+ def days
57
+ self * 86400
58
+ end
59
+
60
+ def ago
61
+ d = self
62
+ return "never" if d <= 0
63
+ return (d).to_s + " seconds" if d < 2*60
64
+ return (d/60).to_s + " minutes" if d < 2*60*60
65
+ return (d/(60*60)).to_s + " hours" if d < 2*60*60*24
66
+ return (d/(60*60*24)).to_s + " days" if d < 2*60*60*24*30
67
+ return (d/(60*60*24*30)).to_s + " months" if d < 2*60*60*24*365
68
+ return (d/(60*60*24*365)).to_s + " years"
69
+ end
70
+ end
71
+
72
+ class String
73
+ def commatize
74
+ return self if size < 5
75
+ self.reverse.gsub(/(\d{3}\B)/, '\1,').reverse
76
+ end
77
+
78
+ def suffixize
79
+ n = self.to_i
80
+ return self + " " if n < 1000*2
81
+ return (n/1000).to_s + " kilo" if n < 1000*1000*2
82
+ return (n/(1000*1000)).to_s + " mega" if n < 1000*1000*1000*2
83
+ return (n/(1000*1000*1000)).to_s + " giga" if n < 1000*1000*1000*1000*2
84
+ return (n/(1000*1000*1000*1000)).to_s + " tera"
85
+ end
86
+ end
87
+
88
+ class Watcher
89
+ attr :stat
90
+ attr :report
91
+
92
+ @@summary_msg = []
93
+ def self.summary
94
+ @@summary_msg.map{ |s| s.gsub!(/^(\S)/, ' \1') }
95
+ @@summary_msg.sort!
96
+ @@summary_msg
97
+ end
98
+
99
+ def initialize(db, dbms, job, name)
100
+ @db = db
101
+ @dbms = dbms
102
+ @report = []
103
+
104
+ report << "Hello" + (name ? " #{name}" : "") + ","
105
+ report << ""
106
+ report << "This is the Bacula Watcher program, reporting on the state of"
107
+ report << "the backup job, #{job}, for your computer."
108
+ report << ""
109
+ full = summarize(job, "F")
110
+ incr = summarize(job, "I")
111
+ status(job, full, incr)
112
+ report.join("\n")
113
+ end
114
+
115
+ def query_job(job, level=nil)
116
+ query = "select starttime, jobfiles, jobbytes, level " +
117
+ "from Job where type='B' and jobstatus='T' and " +
118
+ "name='#{job}' and level='#{level}' order by starttime desc limit 1;"
119
+ if @dbms == "mysql"
120
+ @db.query(query).fetch_row
121
+ else
122
+ @db.exec(query).result[0]
123
+ end
124
+ end
125
+
126
+ def summarize(job, level)
127
+ l = { "I" => "incremental", "F" => "full" }[level] || level
128
+ r = query_job(job, level)
129
+ if r == nil
130
+ report << " - There has never been a #{l} backup performed."
131
+ d = nil
132
+ else
133
+ t = Time.parse(r[0])
134
+ d = (Time.now - t).to_i
135
+ report <<
136
+ " - The last #{l} backup ran #{d.ago} ago,\n" +
137
+ " on #{t.strftime('%A, %B %d, %Y')}.\n" +
138
+ " It contained #{r[2].suffixize}bytes of data " +
139
+ "in #{r[1].commatize} files."
140
+ end
141
+ report << ""
142
+ return d
143
+ end
144
+
145
+ def status(job, full, incr)
146
+ last = incr ? (full && full < incr ? full : incr) : full
147
+ err = []
148
+ if !full
149
+ err << " - The #{job} backup job has never run successfully."
150
+ else
151
+ err << " - The last full backup is too old." if full > 45.days
152
+ err << " - The most recent backup is too old." if last > 15.days
153
+ end
154
+
155
+ if err.empty?
156
+ @stat = "Okay"
157
+ report << "All seems well with the '#{job}' backup job."
158
+ else
159
+ @stat = "ERROR"
160
+ report << "There seems to be a PROBLEM with the '#{job}' backup job."
161
+ [ "", err, "" ].flatten.each do |e| report << e end
162
+ end
163
+ @@summary_msg << "#{stat} for #{job}; " +
164
+ "(Full: #{full.to_i.ago}; Last: #{last.to_i.ago})"
165
+ end
166
+ end
167
+
168
+ def mail_to(addr, subject, msg)
169
+ mail = open("|mail -s '#{subject}' #{addr}", "w")
170
+ mail.puts msg
171
+ mail.close
172
+ end
173
+
174
+ if defined?($help) and $help
175
+ print <<-EOF
176
+ bacuwatch [-help] [-quiet] [-summary] [-notify] [-config <config-file>]
177
+
178
+ -help: Prints this help information.
179
+ -quiet: Disables printing the summary report to standard output.
180
+ -config: Specifies a configuration file.
181
+ -summary: Emails a summary report to each of the addresses
182
+ listed in a "summary" statement.
183
+ -notify: Emails a job report to the email address
184
+ listed in the "watch" statement for that job.
185
+ EOF
186
+ exit
187
+ end
188
+
189
+ if @db_args["dbms"] == "mysql"
190
+ db = Mysql.new(@db_args["host"],
191
+ @db_args["user"], @db_args["password"], @db_args["database"])
192
+ else
193
+ db = PGconn.new(@db_args["host"], 5432, "", "", @db_args["database"],
194
+ @db_args["user"], @db_args["password"])
195
+ end
196
+ @watched.each { |watch_info|
197
+ job, addr, name = watch_info
198
+ w = Watcher.new(db, @db_args["dbms"], job, name)
199
+ subject = "Bacula Watcher: #{w.stat} for #{job}."
200
+ if addr and defined?($notify) and $notify
201
+ mail_to(addr, subject, w.report)
202
+ end
203
+ }
204
+ db.close
205
+
206
+ if defined?($summary) and $summary
207
+ @summaries_to.each { |addr|
208
+ mail_to(addr, "Bacula Watch Summary Report", Watcher::summary)
209
+ }
210
+ end
211
+
212
+ if !defined?($quiet) or !$quiet
213
+ puts Watcher::summary
214
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: bacuwatch
5
+ version: !ruby/object:Gem::Version
6
+ version: "1.5"
7
+ date: 2006-12-03 00:00:00 -05:00
8
+ summary: An app to periodically report on a Bacula backup system.
9
+ require_paths:
10
+ - lib
11
+ email: john@kodis.org
12
+ homepage:
13
+ rubyforge_project:
14
+ description: Bacuwatch is an application normally run from a cron job to email out reports on the status of a series of bacula jobs.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - John Kodis
31
+ files:
32
+ - bacuwatch/bacuwatch
33
+ - bacuwatch/bacuwatch.conf.template
34
+ - bacuwatch/README.bacuwatch
35
+ test_files: []
36
+
37
+ rdoc_options: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ executables:
42
+ - bacuwatch
43
+ extensions: []
44
+
45
+ requirements: []
46
+
47
+ dependencies: []
48
+