bacuwatch 1.5

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,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
+