aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
committerPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
commit89a67770e66d11740948e90a41db6cee0482cf8e (patch)
treebe858515fb789a89d68f94975690ab019813726c /lib
new version.
Diffstat (limited to 'lib')
-rw-r--r--lib/canary.rb55
-rw-r--r--lib/dm-sqlite-adapter-with-mutex.rb28
-rw-r--r--lib/mauve/alert.rb399
-rw-r--r--lib/mauve/alert_changed.rb144
-rw-r--r--lib/mauve/alert_group.rb104
-rw-r--r--lib/mauve/auth_bytemark.rb47
-rw-r--r--lib/mauve/calendar_interface.rb146
-rw-r--r--lib/mauve/configuration.rb484
-rw-r--r--lib/mauve/datamapper.rb13
-rw-r--r--lib/mauve/http_server.rb106
-rw-r--r--lib/mauve/mauve_thread.rb133
-rw-r--r--lib/mauve/mauve_time.rb16
-rw-r--r--lib/mauve/notification.rb165
-rw-r--r--lib/mauve/notifier.rb66
-rw-r--r--lib/mauve/notifiers.rb6
-rw-r--r--lib/mauve/notifiers/debug.rb68
-rw-r--r--lib/mauve/notifiers/email.rb138
-rw-r--r--lib/mauve/notifiers/sms_aql.rb90
-rw-r--r--lib/mauve/notifiers/sms_default.rb12
-rw-r--r--lib/mauve/notifiers/templates/email.html.erb0
-rw-r--r--lib/mauve/notifiers/templates/email.txt.erb0
-rw-r--r--lib/mauve/notifiers/templates/sms.txt.erb0
-rw-r--r--lib/mauve/notifiers/templates/xmpp.html.erb0
-rw-r--r--lib/mauve/notifiers/templates/xmpp.txt.erb1
-rw-r--r--lib/mauve/notifiers/xmpp-smack.rb395
-rw-r--r--lib/mauve/notifiers/xmpp.rb296
-rw-r--r--lib/mauve/people_list.rb44
-rw-r--r--lib/mauve/person.rb230
-rw-r--r--lib/mauve/processor.rb109
-rw-r--r--lib/mauve/proto.rb116
-rw-r--r--lib/mauve/sender.rb94
-rw-r--r--lib/mauve/server.rb142
-rw-r--r--lib/mauve/source_list.rb105
-rw-r--r--lib/mauve/timer.rb86
-rw-r--r--lib/mauve/udp_server.rb109
-rw-r--r--lib/mauve/web_interface.rb334
-rw-r--r--lib/object_builder.rb113
-rw-r--r--lib/rack-flash.rb171
-rw-r--r--lib/sinatra-partials.rb25
39 files changed, 4590 insertions, 0 deletions
diff --git a/lib/canary.rb b/lib/canary.rb
new file mode 100644
index 0000000..afe3a16
--- /dev/null
+++ b/lib/canary.rb
@@ -0,0 +1,55 @@
+# encoding: UTF-8
+require 'logger'
+
+# A little canary class to make sure that threads are are overloaded.
+class Canary
+
+ # Accessor.
+ attr_reader :sleep_time
+
+ # Accessor.
+ attr_reader :threshold
+
+ # Default constructor.
+ def initialize (st=1, log=nil)
+ if Float != st.class and Fixnum != st.class
+ raise ArgumentError.new(
+ "Expected either Fixnum or Float for time to sleep, got #{st.class}.")
+ end
+ @sleep_time = st
+ @threshold = (0.05 * @sleep_time) + @sleep_time
+ @logger = log
+ end
+
+ # Runs the check.
+ def run
+ loop do
+ self.do_test()
+ end
+ end
+
+ def do_test
+ time_start = Time.now
+ sleep(@sleep_time)
+ time_end = Time.now
+ time_elapsed = (time_end - time_start).abs
+ if @threshold < time_elapsed
+ @logger.fatal("Time elapsed is #{time_elapsed} > #{@threshold} therefore Canary is dead.")
+ return false
+ else
+ @logger.debug("Time elapsed is #{time_elapsed} < #{@threshold} therefore Canary is alive.")
+ return true
+ end
+ end
+
+ # Starts a canary in a thread.
+ def self.start (st=1, log=nil)
+ #Thread.abort_on_exception = true
+ thr = Thread.new() do
+ Thread.current[:name] = "Canary Thread"
+ twiti = Canary.new(st, log)
+ twiti.run()
+ end
+ end
+
+end
diff --git a/lib/dm-sqlite-adapter-with-mutex.rb b/lib/dm-sqlite-adapter-with-mutex.rb
new file mode 100644
index 0000000..2842c5e
--- /dev/null
+++ b/lib/dm-sqlite-adapter-with-mutex.rb
@@ -0,0 +1,28 @@
+#
+# Add a mutex so that we can avoid the 'database is locked' Sqlite3Error
+# exception.
+#
+require 'dm-sqlite-adapter'
+require 'monitor'
+
+ADAPTER = DataMapper::Adapters::SqliteAdapter
+
+# better way to alias a private method? (other than "don't"? :) )
+ADAPTER.__send__(:alias_method, :initialize_old, :initialize)
+ADAPTER.__send__(:undef_method, :initialize)
+ADAPTER.__send__(:alias_method, :with_connection_old, :with_connection)
+ADAPTER.__send__(:undef_method, :with_connection)
+
+class ADAPTER
+
+ def initialize(*a)
+ extend(MonitorMixin)
+ initialize_old(*a)
+ end
+
+ private
+
+ def with_connection(&block)
+ synchronize { with_connection_old(&block) }
+ end
+end
diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb
new file mode 100644
index 0000000..374762d
--- /dev/null
+++ b/lib/mauve/alert.rb
@@ -0,0 +1,399 @@
+require 'mauve/proto'
+require 'mauve/alert_changed'
+require 'mauve/datamapper'
+
+
+module Mauve
+ class AlertEarliestDate
+
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :alert_id, Integer
+ property :earliest, DateTime
+ belongs_to :alert, :model => "Alert"
+
+ # 1) Shame we can't get this called automatically from DataMapper.auto_upgrade!
+ #
+ # 2) Can't use a neater per-connection TEMPORARY VIEW because the pooling
+ # function causes the connection to get dropped occasionally, and we can't
+ # hook the reconnect function (that I know of).
+ #
+ # http://www.mail-archive.com/datamapper@googlegroups.com/msg02314.html
+ #
+ def self.create_view!
+ the_distant_future = MauveTime.now + 86400000 # it is the year 2000 - the humans are dead
+ ["BEGIN TRANSACTION",
+ "DROP VIEW IF EXISTS mauve_alert_earliest_dates",
+ "CREATE VIEW
+ mauve_alert_earliest_dates
+ AS
+ SELECT
+ id AS alert_id,
+ NULLIF(
+ MIN(
+ IFNULL(will_clear_at, '#{the_distant_future}'),
+ IFNULL(will_raise_at, '#{the_distant_future}'),
+ IFNULL(will_unacknowledge_at, '#{the_distant_future}')
+ ),
+ '#{the_distant_future}'
+ ) AS earliest
+ FROM mauve_alerts
+ WHERE
+ will_clear_at IS NOT NULL OR
+ will_raise_at IS NOT NULL OR
+ will_unacknowledge_at IS NOT NULL
+ ",
+ "END TRANSACTION"].each do |statement|
+ repository(:default).adapter.execute(statement.gsub(/\s+/, " "))
+ end
+ end
+
+ end
+
+ class Alert
+ def bytesize; 99; end
+ def size; 99; end
+
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :alert_id, String, :required => true, :unique_index => :alert_index, :length=>256
+ property :source, String, :required => true, :unique_index => :alert_index, :length=>512
+ property :subject, String, :length=>512, :length=>512
+ property :summary, String, :length=>1024
+ property :detail, Text, :length=>65535
+ property :importance, Integer, :default => 50
+
+ property :raised_at, DateTime
+ property :cleared_at, DateTime
+ property :updated_at, DateTime
+ property :acknowledged_at, DateTime
+ property :acknowledged_by, String
+ property :update_type, String
+
+ property :will_clear_at, DateTime
+ property :will_raise_at, DateTime
+ property :will_unacknowledge_at, DateTime
+# property :will_unacknowledge_after, Integer
+
+ has n, :changes, :model => AlertChanged
+ has 1, :alert_earliest_date
+
+ validates_with_method :check_dates
+
+ def to_s
+ "#<Alert:#{id} #{alert_id} from #{source} update_type #{update_type}>"
+ end
+
+ def check_dates
+ bad_dates = self.attributes.find_all do |key, value|
+ value.is_a?(DateTime) and not (DateTime.new(2000,1,1,0,0,0)..DateTime.new(2020,1,1,0,0,0)).include?(value)
+ end
+
+ if bad_dates.empty?
+ true
+ else
+ [ false, "The dates "+bad_dates.collect{|k,v| "#{v.to_s} (#{k})"}.join(", ")+" are invalid." ]
+ end
+ end
+
+ default_scope(:default).update(:order => [:source, :importance])
+
+ def logger
+ Log4r::Logger.new(self.class.to_s)
+ end
+
+ def time_relative(secs)
+ secs = secs.to_i.abs
+ case secs
+ when 0..59 then "just now"
+ when 60..3599 then "#{secs/60}m ago"
+ when 3600..86399 then "#{secs/3600}h ago"
+ else
+ days = secs/86400
+ days == 1 ? "yesterday" : "#{secs/86400} days ago"
+ end
+ end
+
+ def summary_one_line
+ subject ? "#{subject} #{summary}" : "#{source} #{summary}"
+ end
+
+ def summary_two_lines
+ msg = ""
+ msg += "from #{source} " if source != subject
+ if cleared_at
+ msg += "cleared #{time_relative(MauveTime.now - cleared_at.to_time)}"
+ elsif acknowledged_at
+ msg += "acknowledged #{time_relative(MauveTime.now - acknowledged_at.to_time)} by #{acknowledged_by}"
+ else
+ msg += "raised #{time_relative(MauveTime.now - raised_at.to_time)}"
+ end
+ [summary_one_line, msg]
+ end
+
+ # Returns a better array with information about the alert.
+ #
+ # @return [Array] An array of three elements: status, message, source.
+ def summary_three_lines
+ status = String.new
+ if "cleared" == update_type
+ status += "CLEARED #{time_relative(MauveTime.now - cleared_at.to_time)}"
+ elsif "acknowledged" == update_type
+ status += "ACKNOWLEDGED #{time_relative(MauveTime.now - acknowledged_at.to_time)} by #{acknowledged_by}"
+ elsif "changed" == update_type
+ status += "CHANGED #{time_relative(MauveTime.now - updated_at.to_time)}"
+ else
+ status += "RAISED #{time_relative(MauveTime.now - raised_at.to_time)}"
+ end
+ src = (source != subject)? "from #{source}" : nil
+ return [status, summary_one_line, src]
+=begin
+ status = String.new
+ if cleared_at
+ status += "CLEARED #{time_relative(MauveTime.now - cleared_at.to_time)}"
+ elsif acknowledged_at
+ status += "ACKNOWLEDGED #{time_relative(MauveTime.now - acknowledged_at.to_time)} by #{acknowledged_by}"
+ else
+ status += "RAISED #{time_relative(MauveTime.now - raised_at.to_time)}"
+ end
+ src = (source != subject)? "from #{source}" : nil
+ return [status, summary_one_line, src]
+=end
+ end
+
+
+ def alert_group
+ AlertGroup.matches(self)[0]
+ end
+
+ def subject
+ attribute_get(:subject) || source
+ end
+
+ def subject=(subject); set_changed_if_different(:subject, subject); end
+ def summary=(summary); set_changed_if_different(:summary, summary); end
+ def detail=(detail); set_changed_if_different(:detail, detail); end
+
+ protected
+ def set_changed_if_different(attribute, value)
+ return if self.__send__(attribute) == value
+ self.update_type ||= :changed
+ attribute_set(attribute.to_sym, value)
+ end
+
+ public
+
+ def acknowledge!(person)
+ self.acknowledged_by = person.username
+ self.acknowledged_at = MauveTime.now
+ self.update_type = :acknowledged
+ self.will_unacknowledge_at = MauveTime.parse(acknowledged_at.to_s) +
+ logger.error("Couldn't save #{self}") unless save
+ AlertGroup.notify([self])
+ end
+
+ def unacknowledge!
+ self.acknowledged_by = nil
+ self.acknowledged_at = nil
+ self.update_type = :raised
+ logger.error("Couldn't save #{self}") unless save
+ AlertGroup.notify([self])
+ end
+
+ def raise!
+ already_raised = raised? && !acknowledged?
+ self.acknowledged_by = nil
+ self.acknowledged_at = nil
+ self.will_unacknowledge_at = nil
+ self.will_raise_at = nil
+ self.update_type = :raised
+ self.raised_at = MauveTime.now
+ self.cleared_at = nil
+ logger.error("Couldn't save #{self}") unless save
+ AlertGroup.notify([self]) unless already_raised
+ end
+
+ def clear!(notify=true)
+ already_cleared = cleared?
+ self.cleared_at = MauveTime.now
+ self.will_clear_at = nil
+ self.update_type = :cleared
+ logger.error("Couldn't save #{self}") unless save
+ AlertGroup.notify([self]) unless !notify || already_cleared
+ end
+
+ # Returns the time at which a timer loop should call poll_event to either
+ # raise, clear or unacknowldge this event.
+ #
+ def due_at
+ o = [will_clear_at, will_raise_at, will_unacknowledge_at].compact.sort[0]
+ o ? o.to_time : nil
+ end
+
+ def poll
+ raise! if will_unacknowledge_at && will_unacknowledge_at.to_time <= MauveTime.now ||
+ will_raise_at && will_raise_at.to_time <= MauveTime.now
+ clear! if will_clear_at && will_clear_at.to_time <= MauveTime.now
+ end
+
+ def raised?
+ !raised_at.nil? && cleared_at.nil?
+ end
+
+ def acknowledged?
+ !acknowledged_at.nil?
+ end
+
+ def cleared?
+ new? || !cleared_at.nil?
+ end
+
+ class << self
+
+ def all_current
+ all(:cleared_at => nil)
+ end
+
+ # Returns the next Alert that will have a timed action due on it, or nil
+ # if none are pending.
+ #
+ def find_next_with_event
+ earliest_alert = AlertEarliestDate.first(:order => [:earliest])
+ earliest_alert ? earliest_alert.alert : nil
+ end
+
+ def all_overdue(at = MauveTime.now)
+ AlertEarliestDate.all(:earliest.lt => at, :order => [:earliest]).collect do |earliest_alert|
+ earliest_alert ? earliest_alert.alert : nil
+ end
+ end
+
+ # Receive an AlertUpdate buffer from the wire.
+ #
+ def receive_update(update, reception_time = MauveTime.now)
+ update = Proto::AlertUpdate.parse_from_string(update) unless
+ update.kind_of?(Proto::AlertUpdate)
+ alerts_updated = []
+
+ logger.debug("Alert update received from wire: #{update.inspect.split.join(", ")}")
+
+ #
+ # Transmission time helps us determine any time offset
+ #
+ if update.transmission_time
+ transmission_time = MauveTime.at(update.transmission_time)
+ else
+ transmission_time = reception_time
+ end
+
+ time_offset = (reception_time - transmission_time).round
+ logger.debug("Update received from a host #{time_offset}s behind") if time_offset.abs > 0
+
+ # Update each alert supplied
+ #
+ update.alert.each do |alert|
+ # Infer some actions from our pure data structure (hmm, wonder if
+ # this belongs in our protobuf-derived class?
+ #
+ raise_time = alert.raise_time == 0 ? nil : MauveTime.at(alert.raise_time + time_offset)
+ clear_time = alert.clear_time == 0 ? nil : MauveTime.at(alert.clear_time + time_offset)
+
+ logger.debug("received at #{reception_time}, transmitted at #{transmission_time}, raised at #{raise_time}, clear at #{clear_time}")
+
+ do_clear = clear_time && clear_time <= reception_time
+ do_raise = raise_time && raise_time <= reception_time
+
+ alert_db = first(:alert_id => alert.id, :source => update.source) ||
+ new(:alert_id => alert.id, :source => update.source)
+
+ pre_raised = alert_db.raised?
+ pre_cleared = alert_db.cleared?
+ pre_acknowledged = alert_db.acknowledged?
+
+ alert_db.update_type = nil
+
+ ##
+ #
+ # Allow a 15s offset in timings.
+ #
+ if raise_time
+ if raise_time <= (reception_time + 15)
+ alert_db.raised_at = raise_time
+ else
+ alert_db.will_raise_at = raise_time
+ end
+ end
+
+ if clear_time
+ if clear_time <= (reception_time + 15)
+ alert_db.cleared_at = clear_time
+ else
+ alert_db.will_clear_at = clear_time
+ end
+ end
+
+ # re-raise
+ if alert_db.cleared_at && alert_db.raised_at && alert_db.cleared_at < alert_db.raised_at
+ alert_db.cleared_at = nil
+ end
+
+ if pre_cleared && alert_db.raised?
+ alert_db.update_type = :raised
+ elsif pre_raised && alert_db.cleared?
+ alert_db.update_type = :cleared
+ end
+
+ # Changing any of these attributes causes the alert to be sent back
+ # out to the notification system with an update_type of :changed.
+ #
+ alert_db.subject = alert.subject if alert.subject && !alert.subject.empty?
+ alert_db.summary = alert.summary if alert.summary && !alert.summary.empty?
+ alert_db.detail = alert.detail if alert.detail && !alert.detail.empty?
+
+ # These updates happen but do not sent the alert back to the
+ # notification system.
+ #
+ alert_db.importance = alert.importance if alert.importance != 0
+
+ # FIXME: this logic ought to be clearer as it may get more complicated
+ #
+ if alert_db.update_type
+ if alert_db.update_type.to_sym == :changed && !alert_db.raised?
+ # do nothing
+ else
+ alerts_updated << alert_db
+ end
+ else
+ alert_db.update_type = :changed
+ end
+
+ logger.error "Couldn't save update #{alert} because of #{alert_db.errors}" unless alert_db.save
+ end
+
+ # If this is a complete replacement update, find the other alerts
+ # from this source and clear them.
+ #
+ if update.replace
+ alert_ids_mentioned = update.alert.map { |alert| alert.id }
+ logger.debug "Replacing all alerts from #{update.source} except "+alert_ids_mentioned.join(",")
+ all(:source => update.source,
+ :alert_id.not => alert_ids_mentioned,
+ :cleared_at => nil
+ ).each do |alert_db|
+ logger.debug "Replace: clearing #{alert_db.id}"
+ alert_db.clear!(false)
+ alerts_updated << alert_db
+ end
+ end
+
+ AlertGroup.notify(alerts_updated)
+ end
+
+ def logger
+ Log4r::Logger.new("Mauve::Alert")
+ end
+ end
+ end
+end
diff --git a/lib/mauve/alert_changed.rb b/lib/mauve/alert_changed.rb
new file mode 100644
index 0000000..4668fd7
--- /dev/null
+++ b/lib/mauve/alert_changed.rb
@@ -0,0 +1,144 @@
+# encoding: UTF-8
+require 'mauve/datamapper'
+require 'log4r'
+
+module Mauve
+ class AlertChanged
+ include DataMapper::Resource
+
+ # so .first always returns the most recent update
+ default_scope(:default).update(:order => [:at.desc, :id.desc])
+
+ property :id, Serial
+ property :alert_id, Integer, :required => true
+ property :person, String, :required => true
+ property :at, DateTime, :required => true
+ property :was_relevant, Boolean, :required => true, :default => true
+ property :level, String, :required => true
+ property :update_type, String, :required => true
+ property :remind_at, DateTime
+ property :updated_at, DateTime
+
+
+ def to_s
+ "#<AlertChanged:#{id} of #{alert_id} for #{person} update_type #{update_type}>"
+ end
+
+ belongs_to :alert
+
+ # There is a bug there. You could have two reminders for the same
+ # person if that person has two different notify clauses.
+ #
+ # See the test cases test_Bug_reminders_get_trashed() in ./test/
+ after :create do
+ old_changed = AlertChanged.first(
+ :alert_id => alert_id,
+ :person => person,
+ :id.not => id,
+ :remind_at.not => nil
+ )
+ if old_changed
+ if !old_changed.update(:remind_at => nil)
+ logger.error "Couldn't save #{old_changed}, will get duplicate reminders"
+ end
+ end
+ end
+
+ def was_relevant=(value)
+ attribute_set(:was_relevant, value)
+ end
+
+ def logger
+ Log4r::Logger.new self.class.to_s
+ end
+
+## def initialize
+# logger = Log4r::Logger.new self.class.to_s
+# end
+
+ ## Checks to see if a raise was send to the person.
+ #
+ # @TODO: Recurence is broken in ruby, change this so that it does not
+ # use it.
+ #
+ # @author Matthew Bloch
+ # @return [Boolean] true if it was relevant, false otherwise.
+ def was_relevant_when_raised?
+ if :acknowledged == update_type.to_sym and true == was_relevant
+ return true
+ end
+ return was_relevant if update_type.to_sym == :raised
+ previous = AlertChanged.first(:id.lt => id,
+ :alert_id => alert_id,
+ :person => person)
+ if previous
+ previous.was_relevant_when_raised?
+ else
+ # a bug, but hardly inconceivable :)
+ logger.warn("Could not see that #{alert} was raised with #{person} "+
+ "but further updates exist (e.g. #{self}) "+
+ "- you may see spurious notifications as a result")
+ true
+ end
+ end
+
+ # Sends a reminder about this alert state change, or forget about it if
+ # the alert has been acknowledged
+ #
+ def remind
+ logger.debug "Reminding someone about #{self.inspect}"
+
+ alert_group = AlertGroup.matches(alert)[0]
+
+ if !alert_group || alert.acknowledged?
+ logger.debug((alert_group ?
+ "Alert already acknowledged" :
+ "No alert group matches any more"
+ ) + ", no reminder due"
+ )
+ self.remind_at = nil
+ save
+ else
+ saved = false
+
+ alert_group.notifications.each do |notification|
+ notification.people.each do |person|
+ if person.username == self.person
+ person.remind(alert, level)
+ self.remind_at = notification.remind_at_next(alert)
+ save
+ saved = true
+ end
+ end
+ end
+
+ if !saved
+ logger.warn("#{self.inspect} did not match any people, maybe configuration has changed but I'm going to delete this and not try to remind anyone again")
+ destroy!
+ end
+ end
+ end
+
+ def due_at # mimic interface from Alert
+ remind_at ? remind_at.to_time : nil
+ end
+
+ def poll # mimic interface from Alert
+ remind if remind_at.to_time <= MauveTime.now
+ end
+
+ class << self
+ def next_reminder
+ first(:remind_at.not => nil, :order => [:remind_at])
+ end
+
+ def find_next_with_event # mimic interface from Alert
+ next_reminder
+ end
+
+ def all_overdue(at = MauveTime.now)
+ all(:remind_at.not => nil, :remind_at.lt => at, :order => [:remind_at]).to_a
+ end
+ end
+ end
+end
diff --git a/lib/mauve/alert_group.rb b/lib/mauve/alert_group.rb
new file mode 100644
index 0000000..d8156fa
--- /dev/null
+++ b/lib/mauve/alert_group.rb
@@ -0,0 +1,104 @@
+# encoding: UTF-8
+require 'mauve/alert'
+require 'log4r'
+
+module Mauve
+ class AlertGroup < Struct.new(:name, :includes, :acknowledgement_time, :level, :notifications)
+ def to_s
+ "#<AlertGroup:#{name} (level #{level})>"
+ end
+
+ class << self
+ def matches(alert)
+ all.select { |alert_group| alert_group.matches_alert?(alert) }
+ end
+
+ # If there is any significant change to a set of alerts, the Alert
+ # class sends the list here so that appropriate action can be taken
+ # for each one. We scan the list of alert groups to find out which
+ # alerts match which groups, then send a notification to each group
+ # object in turn.
+ #
+ def notify(alerts)
+ alerts.each do |alert|
+ groups = matches(alert)
+
+ #
+ # Make sure we've got a matching group
+ #
+ logger.warn "no groups found for #{alert.id}" if groups.empty?
+
+ #
+ # Notify each group.
+ #
+ groups.each do |grp|
+ logger.info("notifying group #{groups[0]} of AlertID.#{alert.id}.")
+ grp.notify(alert)
+ end
+ end
+ end
+
+ def logger
+ Log4r::Logger.new self.to_s
+ end
+
+ def all
+ Configuration.current.alert_groups
+ end
+
+ # Find all alerts that match
+ #
+ # @deprecated Buggy method, use Alert.get_all().
+ #
+ # This method returns all the alerts in all the alert_groups. Only
+ # the first one should be returned thus making this useless. If you want
+ # a list of all the alerts matching a level, use Alert.get_all().
+ #
+ def all_alerts_by_level(level)
+ Configuration.current.alert_groups.map do |alert_group|
+ alert_group.level == level ? alert_group.current_alerts : []
+ end.flatten.uniq
+ end
+
+ end
+
+ def initialize(name)
+ self.name = name
+ self.level = :normal
+ self.includes = Proc.new { true }
+ end
+
+ # The list of current raised alerts in this group.
+ #
+ def current_alerts
+ Alert.all(:cleared_at => nil, :raised_at.not => nil).select { |a| matches_alert?(a) }
+ end
+
+ # Decides whether a given alert belongs in this group according to its
+ # includes { } clause
+ #
+ # @param [Alert] alert An alert to test for belongness to group.
+ # @return [Boolean] Success or failure.
+ def matches_alert?(alert)
+ result = alert.instance_eval(&self.includes)
+ if true == result or
+ true == result.instance_of?(MatchData)
+ return true
+ end
+ return false
+ end
+
+ def logger ; self.class.logger ; end
+
+ # Signals that a given alert (which is assumed to belong in this group)
+ # has undergone a significant change. We resend this to every notify list.
+ #
+ def notify(alert)
+ notifications.each do |notification|
+ notification.alert_changed(alert)
+ end
+ end
+
+ end
+
+end
diff --git a/lib/mauve/auth_bytemark.rb b/lib/mauve/auth_bytemark.rb
new file mode 100644
index 0000000..7419d10
--- /dev/null
+++ b/lib/mauve/auth_bytemark.rb
@@ -0,0 +1,47 @@
+# encoding: UTF-8
+require 'sha1'
+require 'xmlrpc/client'
+require 'timeout'
+
+class AuthSourceBytemark
+
+ def initialize (srv='auth.bytemark.co.uk', port=443)
+ raise ArgumentError.new("Server must be a String, not a #{srv.class}") if String != srv.class
+ raise ArgumentError.new("Port must be a Fixnum, not a #{port.class}") if Fixnum != port.class
+ @srv = srv
+ @port = port
+ @timeout = 7
+ end
+
+ ## Not really needed.
+ def ping ()
+ begin
+ MauveTimeout.timeout(@timeout) do
+ s = TCPSocket.open(@srv, @port)
+ s.close()
+ return true
+ end
+ rescue MauveTimeout::Error => ex
+ return false
+ rescue => ex
+ return false
+ end
+ return false
+ end
+
+ def authenticate(login, password)
+ raise ArgumentError.new("Login must be a string, not a #{login.class}") if String != login.class
+ raise ArgumentError.new("Password must be a string, not a #{password.class}") if String != password.class
+ raise ArgumentError.new("Login or/and password is/are empty.") if login.empty? || password.empty?
+ client = XMLRPC::Client.new(@srv,"/",@port,nil,nil,nil,nil,true,@timeout).proxy("bytemark.auth")
+ begin
+ challenge = client.getChallengeForUser(login)
+ response = Digest::SHA1.new.update(challenge).update(password).hexdigest
+ client.login(login, response)
+ rescue XMLRPC::FaultException => fault
+ return "Fault code is #{fault.faultCode} stating #{fault.faultString}"
+ end
+ return true
+ end
+
+end
diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb
new file mode 100644
index 0000000..08cfab3
--- /dev/null
+++ b/lib/mauve/calendar_interface.rb
@@ -0,0 +1,146 @@
+# encoding: UTF-8
+require 'log4r'
+require 'net/http'
+require 'net/https'
+require 'uri'
+
+module Mauve
+
+ # Interface to the Bytemark calendar.
+ #
+ # Nota Bene that this does not include a chaching mechanism. Some caching
+ # is implemented in the Person object.
+ #
+ # @see Mauve::Person
+ # @author yann Golanski.
+ class CalendarInterface
+
+ TIMEOUT = 7
+
+ public
+
+ # Gets a list of ssologin on support.
+ #
+ # Class method.
+ #
+ # @param [String] url A Calendar API url.
+ # @return [Array] A list of all the username on support.
+ def self.get_users_on_support(url)
+ result = get_URL(url)
+ logger = Log4r::Logger.new "mauve::CalendarInterface"
+ logger.debug("Cheching who is on support: #{result}")
+ return result
+ end
+
+ # Check to see if the user is on support.
+ #
+ # Class method.
+ #
+ # @param [String] url A Calendar API url.
+ # @param [String] usr User single sign on.
+ # @return [Boolean] True if on support, false otherwise.
+ def self.is_user_on_support?(url, usr)
+ logger = Log4r::Logger.new "mauve::CalendarInterface"
+ list = get_URL(url)
+ if true == list.include?("nobody")
+ logger.error("Nobody is on support thus alerts are ignored.")
+ return false
+ end
+ result = list.include?(usr)
+ logger.debug("Cheching if #{usr} is on support: #{result}")
+ return result
+ end
+
+ # Check to see if the user is on holiday.
+ #
+ # Class method.
+ #
+ # @param [String] url A Calendar API url.
+ # @param [String] usr User single sign on.
+ # @return [Boolean] True if on holiday, false otherwise.
+ def self.is_user_on_holiday?(url, usr)
+ list = get_URL(url)
+ return false if true == list.nil? or true == list.empty?
+ pattern = /[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}:[\d]{2}/
+ result = (list[0].match(pattern))? true : false
+ logger = Log4r::Logger.new "mauve::CalendarInterface"
+ logger.debug("Cheching if #{usr} is on holiday: #{result}")
+ return result
+ end
+
+
+ private
+
+ # Gets a URL from the wide web.
+ #
+ # Must NOT crash Mauveserver.
+ #
+ # Class method.
+ #
+ # @TODO: boot this in its own class since list of ips will need it too.
+ #
+ # @param [String] url A Calendar API url.
+ # @retur [Array] An array of strings, each newline creates an new item.
+ def self.get_URL (uri_str, limit = 11)
+
+ logger = Log4r::Logger.new "mauve::CalendarInterface"
+
+ if 0 == limit
+ logger.warn("HTTP redirect deeper than 11 on #{uri_str}.")
+ return false
+ end
+
+ begin
+ uri_str = 'http://' + uri_str unless uri_str.match(uri_str)
+ url = URI.parse(uri_str)
+ http = Net::HTTP.new(url.host, url.port)
+ http.open_timeout = TIMEOUT
+ http.read_timeout = TIMEOUT
+ if (url.scheme == "https")
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ response = nil
+ if nil == url.query
+ response = http.start { http.get(url.path) }
+ else
+ response = http.start { http.get("#{url.path}?#{url.query}") }
+ end
+ case response
+ when Net::HTTPRedirection
+ then
+ newURL = response['location'].match(/^http/)?
+ response['Location']:
+ uri_str+response['Location']
+ self.getURL(newURL, limit-1)
+ else
+ return response.body.split("\n")
+ end
+ rescue Errno::EHOSTUNREACH => ex
+ logger.warn("no route to host.")
+ return Array.new
+ rescue MauveTimeout::Error => ex
+ logger.warn("time out reached.")
+ return Array.new
+ rescue ArgumentError => ex
+ unless uri_str.match(/\/$/)
+ logger.debug("Potential missing '/' at the end of hostname #{uri_str}")
+ uri_str += "/"
+ retry
+ else
+ str = "ArgumentError raise: #{ex.message} #{ex.backtrace.join("\n")}"
+ logger.fatal(str)
+ return Array.new
+ #raise ex
+ end
+ rescue => ex
+ str = "ArgumentError raise: #{ex.message} #{ex.backtrace.join("\n")}"
+ logger.fatal(str)
+ return Array.new
+ #raise ex
+ end
+
+ end
+ end
+
+end
diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb
new file mode 100644
index 0000000..b11e1b5
--- /dev/null
+++ b/lib/mauve/configuration.rb
@@ -0,0 +1,484 @@
+# encoding: UTF-8
+require 'object_builder'
+require 'mauve/server'
+require 'mauve/web_interface'
+require 'mauve/person'
+require 'mauve/notification'
+require 'mauve/alert_group'
+require 'mauve/people_list'
+require 'mauve/source_list'
+
+# Seconds, minutes, hours, days, and weeks... More than that, we
+# really should not need it.
+class Integer
+ def seconds; self; end
+ def minutes; self*60; end
+ def hours; self*3600; end
+ def days; self*86400; end
+ def weeks; self*604800; end
+ alias_method :day, :days
+ alias_method :hour, :hours
+ alias_method :minute, :minutes
+ alias_method :week, :weeks
+end
+
+module Mauve
+
+ ## Configuration object for Mauve.
+ #
+ #
+ # @TODO Write some more documentation. This is woefully inadequate.
+ #
+ #
+ # == How to add a new class to the configuration?
+ #
+ # - Add a method to ConfigurationBuilder such that your new object
+ # maybe created. Call it created_NEW_OBJ.
+ #
+ # - Create a new class inheriting from ObjectBuilder with at least a
+ # builder_setup() method. This should create the new object you want.
+ #
+ # - Define attributes for the new class are defined as "is_attribute".
+ #
+ # - Methods for the new class are defined as methods or missing_method
+ # depending on what one wishes to do. Remember to define a method
+ # with "instance_eval(&block)" if you want to call said block within
+ # the new class.
+ #
+ # - Add a "is_builder "<name>", BuilderCLASS" clause in the
+ # ConfigurationBuilder class.
+ #
+ # That should be it.
+ #
+ # @author Matthew Bloch, Yann Golanski
+ class Configuration
+
+ class << self
+ attr_accessor :current
+ end
+
+ attr_accessor :server
+ attr_accessor :last_alert_group
+ attr_reader :notification_methods
+ attr_reader :people
+ attr_reader :alert_groups
+ attr_reader :people_lists
+ attr_reader :source_lists
+ attr_reader :logger
+
+ def initialize
+ @notification_methods = {}
+ @people = {}
+ @people_lists = {}
+ @alert_groups = []
+ @source_lists = SourceList.new()
+ @logger = Log4r::Logger.new("Mauve")
+
+ end
+
+ def close
+ server.close
+ end
+
+ end
+
+ class LoggerOutputterBuilder < ObjectBuilder
+
+ def builder_setup(outputter)
+ @outputter = outputter.capitalize+"Outputter"
+
+ begin
+ Log4r.const_get(@outputter)
+ rescue
+ require "log4r/outputter/#{@outputter.downcase}"
+ end
+
+ @args = {}
+ end
+
+ def result
+ @result ||= Log4r.const_get(@outputter).new("Mauve", @args)
+ end
+
+ def format(f)
+ result.formatter = Log4r::PatternFormatter.new(:pattern => f)
+ end
+
+ def method_missing(name, value=nil)
+ if value.nil?
+ result.send(name.to_sym)
+ else
+ @args[name.to_sym] = value
+ end
+ end
+
+ end
+
+ class LoggerBuilder < ObjectBuilder
+
+ is_builder "outputter", LoggerOutputterBuilder
+
+ def builder_setup
+ logger = Log4r::Logger.new("Mauve")
+ @default_format = nil
+ @default_level = Log4r::RootLogger.instance.level
+ end
+
+ def result
+ @result = Log4r::Logger['Mauve']
+ end
+
+ def default_format(f)
+ @default_formatter = Log4r::PatternFormatter.new(:pattern => f)
+ #
+ # Set all current outputters
+ #
+ result.outputters.each do |o|
+ o.formatter = @default_formatter if o.formatter.is_a?(Log4r::DefaultFormatter)
+ end
+ end
+
+ def default_level(l)
+ if Log4r::Log4rTools.valid_level?(l)
+ @default_level = l
+ else
+ raise "Bad default level set for the logger #{l}.inspect"
+ end
+
+ result.outputters.each do |o|
+ o.level = @default_level if o.level == Log4r::RootLogger.instance.level
+ end
+ end
+
+ def created_outputter(outputter)
+ #
+ # Set the formatter and level for any newly created outputters
+ #
+ if @default_formatter
+ outputter.formatter = @default_formatter if outputter.formatter.is_a?(Log4r::DefaultFormatter)
+ end
+
+ if @default_level
+ outputter.level = @default_level if outputter.level == Log4r::RootLogger.instance.level
+ end
+
+ result.outputters << outputter
+ end
+
+ end
+
+ class ProcessorBuilder < ObjectBuilder
+ is_attribute "sleep_interval"
+
+ def builder_setup
+ @result = Processor.instance
+ end
+
+ def method_missing(name, value)
+ @args[name] = value
+ end
+ end
+
+ class UDPServerBuilder < ObjectBuilder
+ is_attribute "port"
+ is_attribute "ip"
+ is_attribute "sleep_interval"
+
+ def builder_setup
+ @result = UDPServer.instance
+ end
+
+ def method_missing(name, value)
+ @args[name] = value
+ end
+ end
+
+ class TimerBuilder < ObjectBuilder
+ is_attribute "sleep_interval"
+
+ def builder_setup
+ @result = Timer.instance
+ end
+
+ def method_missing(name, value)
+ @args[name] = value
+ end
+
+
+ end
+
+ class HTTPServerBuilder < ObjectBuilder
+
+ is_attribute "port"
+ is_attribute "ip"
+ is_attribute "document_root"
+
+ def builder_setup
+ @result = HTTPServer.instance
+ end
+
+ def method_missing(name, value)
+ @args[name] = value
+ end
+
+ end
+
+ class NotifierBuilder < ObjectBuilder
+ is_attribute "sleep_interval"
+
+ def builder_setup
+ @result = Notifier.instance
+ end
+
+ def method_missing(name, value)
+ @args[name] = value
+ end
+
+ end
+
+ class ServerBuilder < ObjectBuilder
+
+ is_builder "web_interface", HTTPServerBuilder
+ is_builder "listener", UDPServerBuilder
+ is_builder "processor", ProcessorBuilder
+ is_builder "timer", TimerBuilder
+ is_builder "notifier", NotifierBuilder
+
+ def builder_setup
+ @args = {}
+ end
+
+ def result
+ @result = Mauve::Server.instance
+ @result.configure(@args)
+ @result.web_interface = @web_interface
+ @result
+ end
+
+ def method_missing(name, value)
+ @args[name] = value
+ end
+
+ def created_web_interface(web_interface)
+ @web_interface = web_interface
+ end
+
+ def created_listener(listener)
+ @listener = listener
+ end
+
+ def created_processor(processor)
+ @processor = processor
+ end
+
+ def created_notifier(notifier)
+ @notifier = notifier
+ end
+ end
+
+ class NotificationMethodBuilder < ObjectBuilder
+
+ def builder_setup(name)
+ @notification_type = name.capitalize
+ @name = name
+ provider("Default")
+ end
+
+
+ def provider(name)
+ notifiers_base = Mauve::Notifiers
+ notifiers_type = notifiers_base.const_get(@notification_type)
+ @provider_class = notifiers_type.const_get(name)
+ end
+
+ def result
+ @result ||= @provider_class.new(@name)
+ end
+
+ def method_missing(name, value=nil)
+ if value
+ result.send("#{name}=".to_sym, value)
+ else
+ result.send(name.to_sym)
+ end
+ end
+
+ end
+
+ class PersonBuilder < ObjectBuilder
+
+ def builder_setup(username)
+ @result = Person.new(username)
+ end
+
+ is_block_attribute "urgent"
+ is_block_attribute "normal"
+ is_block_attribute "low"
+
+ def all(&block); urgent(&block); normal(&block); low(&block); end
+
+ def password (pwd)
+ @result.password = pwd.to_s
+ end
+
+ def holiday_url (url)
+ @result.holiday_url = url
+ end
+
+ def suppress_notifications_after(h)
+ raise ArgumentError.new("notification_threshold must be specified as e.g. (10 => 1.minute)") unless
+ h.kind_of?(Hash) && h.keys[0].kind_of?(Integer) && h.values[0].kind_of?(Integer)
+ @result.notification_thresholds[h.values[0]] = Array.new(h.keys[0])
+ end
+
+ end
+
+ class NotificationBuilder < ObjectBuilder
+
+ def builder_setup(*who)
+ who = who.map do |username|
+ #raise BuildException.new("You haven't declared who #{username} is") unless
+ # @context.people[username]
+ #@context.people[username]
+ if @context.people[username]
+ @context.people[username]
+ elsif @context.people_lists[username]
+ @context.people_lists[username]
+ else
+ raise BuildException.new("You have not declared who #{username} is")
+ end
+ end
+ @result = Notification.new(who, @context.last_alert_group.level)
+ end
+
+ is_attribute "every"
+ is_block_attribute "during"
+ ##is_attribute "hours_in_day"
+ ##is_attribute "unacknowledged"
+
+ end
+
+ class AlertGroupBuilder < ObjectBuilder
+
+ def builder_setup(name=anonymous_name)
+ @result = AlertGroup.new(name)
+ @context.last_alert_group = @result
+ end
+
+ is_block_attribute "includes"
+ is_attribute "acknowledgement_time"
+ is_attribute "level"
+
+ is_builder "notify", NotificationBuilder
+
+ def created_notify(notification)
+ @result.notifications ||= []
+ @result.notifications << notification
+ end
+
+ end
+
+ # New list of persons.
+ # @author Yann Golanski
+ class PeopleListBuilder < ObjectBuilder
+
+ # Create a new instance and adds it.
+ def builder_setup(label)
+ pp label
+ @result = PeopleList.new(label)
+ end
+
+ is_attribute "list"
+
+ end
+
+ # New list of sources.
+ # @author Yann Golanski
+ class AddSourceListBuilder < ObjectBuilder
+
+ # Create the temporary object.
+ def builder_setup(label)
+ @result = AddSoruceList.new(label)
+ end
+
+ # List of IP addresses or hostnames.
+ is_attribute "list"
+
+ end
+
+
+ # this should live in AlertGroupBuilder but can't due to
+ # http://briancarper.net/blog/ruby-instance_eval_constant_scoping_broken
+ #
+ module ConfigConstants
+ URGENT = :urgent
+ NORMAL = :normal
+ LOW = :low
+ end
+
+ class ConfigurationBuilder < ObjectBuilder
+
+ include ConfigConstants
+
+ is_builder "server", ServerBuilder
+ is_builder "notification_method", NotificationMethodBuilder
+ is_builder "person", PersonBuilder
+ is_builder "alert_group", AlertGroupBuilder
+ is_builder "people_list", PeopleListBuilder
+ is_builder "add_source_list", AddSourceListBuilder
+ is_builder "logger", LoggerBuilder
+
+ def initialize
+ @context = @result = Configuration.new
+ # FIXME: need to test blocks that are not immediately evaluated
+ end
+
+ def created_server(server)
+ raise ArgumentError.new("Only one 'server' clause can be specified") if
+ @result.server
+ @result.server = server
+ end
+
+ def created_notification_method(notification_method)
+ name = notification_method.name
+ raise BuildException.new("Duplicate notification '#{name}'") if
+ @result.notification_methods[name]
+ @result.notification_methods[name] = notification_method
+ end
+
+ def created_person(person)
+ name = person.username
+ raise BuildException.new("Duplicate person '#{name}'") if
+ @result.people[name]
+ @result.people[person.username] = person
+ end
+
+ def created_alert_group(alert_group)
+ name = alert_group.name
+ raise BuildException.new("Duplicate alert_group '#{name}'") unless
+ @result.alert_groups.select { |g| g.name == name }.empty?
+ @result.alert_groups << alert_group
+ end
+
+ # Create a new instance of people_list.
+ #
+ # @param [PeopleList] people_list The new list of persons.
+ # @return [NULL] nada.
+ def created_people_list(people_list)
+ label = people_list.label
+ raise BuildException.new("Duplicate people_list '#{label}'") if @result.people_lists[label]
+ @result.people_lists[label] = people_list
+ end
+
+ # Create a new list of sources.
+ #
+ # @param [] add_source_list
+ # @return [NULL] nada.
+ def created_add_source_list(add_source_list)
+ @result.source_lists.create_new_list(add_source_list.label,
+ add_source_list.list)
+ end
+
+ end
+
+end
diff --git a/lib/mauve/datamapper.rb b/lib/mauve/datamapper.rb
new file mode 100644
index 0000000..27d89a3
--- /dev/null
+++ b/lib/mauve/datamapper.rb
@@ -0,0 +1,13 @@
+#
+#
+# Small loader to put all our Datamapper requirements together
+#
+#
+require 'dm-core'
+require 'dm-sqlite-adapter-with-mutex'
+require 'dm-types'
+require 'dm-serializer'
+require 'dm-migrations'
+require 'dm-validations'
+require 'dm-timestamps'
+
diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb
new file mode 100644
index 0000000..b4ced32
--- /dev/null
+++ b/lib/mauve/http_server.rb
@@ -0,0 +1,106 @@
+# encoding: UTF-8
+#
+# Bleuurrgggggh! Bleurrrrrgghh!
+#
+require 'digest/sha1'
+require 'log4r'
+require 'thin'
+require 'rack'
+require 'rack-flash'
+require 'rack/handler/webrick'
+require 'mauve/auth_bytemark'
+require 'mauve/web_interface'
+require 'mauve/mauve_thread'
+
+################################################################################
+#
+# Bodge up thin logging.
+#
+module Thin
+ module Logging
+
+ def log(m=nil)
+ # return if Logging.silent?
+ logger = Log4r::Logger.new "Mauve::HTTPServer"
+ logger.info(m || yield)
+ end
+
+ def debug(m=nil)
+ # return unless Logging.debug?
+ logger = Log4r::Logger.new "Mauve::HTTPServer"
+ logger.debug(m || yield)
+ end
+
+ def trace(m=nil)
+ return unless Logging.trace?
+ logger = Log4r::Logger.new "Mauve::HTTPServer"
+ logger.debug(m || yield)
+ end
+
+ def log_error(e=$!)
+ logger = Log4r::Logger.new "Mauve::HTTPServer"
+ logger.error(e)
+ logger.debug(e.backtrace.join("\n"))
+ end
+
+ end
+end
+
+################################################################################
+#
+# More logging hacks for Rack
+#
+# @see http://stackoverflow.com/questions/2239240/use-rackcommonlogger-in-sinatra
+#
+class RackErrorsProxy
+
+ def initialize(l); @logger = l; end
+
+ def write(msg)
+ #@logger.debug "NEXT LOG LINE COURTESY OF: "+caller.join("\n")
+ case msg
+ when String then @logger.info(msg.chomp)
+ when Array then @logger.info(msg.join("\n"))
+ else
+ @logger.error(msg.inspect)
+ end
+ end
+
+ alias_method :<<, :write
+ alias_method :puts, :write
+ def flush; end
+end
+
+
+
+################################################################################
+module Mauve
+
+ #
+ # API to control the web server
+ #
+ class HTTPServer < MauveThread
+
+ include Singleton
+
+ attr_accessor :port, :ip, :document_root
+ attr_accessor :session_secret # not used yet
+
+ def initialize
+ @port = 32761
+ @ip = "127.0.0.1"
+ @document_root = "."
+ @session_secret = rand(2**100).to_s
+ end
+
+ def main_loop
+ @server = ::Thin::Server.new(@ip, @port, Rack::CommonLogger.new(Rack::Chunked.new(Rack::ContentLength.new(WebInterface.new)), RackErrorsProxy.new(@logger)), :signals => false)
+ @server.start
+ end
+
+ def stop
+ @server.stop
+ super
+ end
+ end
+end
diff --git a/lib/mauve/mauve_thread.rb b/lib/mauve/mauve_thread.rb
new file mode 100644
index 0000000..f40c79c
--- /dev/null
+++ b/lib/mauve/mauve_thread.rb
@@ -0,0 +1,133 @@
+require 'thread'
+require 'singleton'
+
+module Mauve
+
+ class MauveThread
+
+ def initialize
+ end
+
+ def logger
+ Log4r::Logger.new(self.class.to_s)
+ end
+
+ def run_thread(interval = 0.2)
+ #
+ # Good to go.
+ #
+ @frozen = false
+ @stop = false
+
+ logger.debug("Started")
+
+ @sleep_interval ||= interval
+
+ while !@stop do
+ #
+ # Schtop!
+ #
+ if @frozen
+ logger.debug("Frozen")
+ Thread.stop
+ logger.debug("Thawed")
+ end
+
+ yield
+
+ next if self.should_stop?
+
+ Kernel.sleep(@sleep_interval)
+ end
+
+ logger.debug("Stopped")
+ end
+
+ def should_stop?
+ @frozen or @stop
+ end
+
+ def freeze
+ logger.debug("Freezing")
+
+ @frozen = true
+
+ 20.times { Kernel.sleep 0.1 ; break if @thread.stop? }
+
+ logger.debug("Thread has not frozen!") unless @thread.stop?
+ end
+
+ def thaw
+ logger.debug("Thawing")
+
+ @frozen = false
+
+ @thread.wakeup if @thread.stop?
+ end
+
+ def start
+ logger.debug("Starting")
+ @thread = Thread.new{ self.run_thread { self.main_loop } }
+ end
+
+ def run
+ if self.alive?
+ self.thaw
+ else
+ self.start
+ end
+ end
+
+ def alive?
+ @thread.is_a?(Thread) and @thread.alive?
+ end
+
+ def join(ok_exceptions=[])
+ begin
+ @thread.join if @thread.is_a?(Thread)
+ rescue StandardError => err
+ logger.debug "#{err.to_s} #{err.class}"
+ Kernel.raise err unless ok_exceptions.any?{|e| err.is_a?(e)}
+ end
+ end
+
+ def raise(ex)
+ @thread.raise(ex)
+ end
+
+ def restart
+ self.stop
+ self.start
+ end
+
+ def stop
+ logger.debug("Stopping")
+
+ @stop = true
+
+ 10.times do
+ break unless self.alive?
+ Kernel.sleep 1 if self.alive?
+ end
+
+ #
+ # OK I've had enough now.
+ #
+ self.kill if self.alive?
+
+ self.join
+ end
+
+ alias exit stop
+
+ def kill
+ logger.debug("Killing")
+ @frozen = true
+ @thread.kill
+ logger.debug("Killed")
+ end
+
+ end
+
+end
+
diff --git a/lib/mauve/mauve_time.rb b/lib/mauve/mauve_time.rb
new file mode 100644
index 0000000..bf69d1b
--- /dev/null
+++ b/lib/mauve/mauve_time.rb
@@ -0,0 +1,16 @@
+
+require 'time'
+
+module Mauve
+
+ class MauveTime < Time
+
+ def to_s
+ self.iso8601
+ end
+
+ end
+
+end
+
+
diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb
new file mode 100644
index 0000000..2220211
--- /dev/null
+++ b/lib/mauve/notification.rb
@@ -0,0 +1,165 @@
+# encoding: UTF-8
+require 'mauve/person'
+require 'mauve/notifiers'
+
+module Mauve
+ # This class provides an execution context for the code found in 'during'
+ # blocks in the configuration file. This code specifies when an alert
+ # should cause notifications to be generated, and can access @time and
+ # @alert variables. There are also some helper methods to provide
+ # oft-needed functionality such as hours_in_day.
+ #
+ # e.g. to send alerts only between 10 and 11 am:
+ #
+ # during = Proc.new { @time.hour == 10 }
+ #
+ # ... later on ...
+ #
+ # DuringRunner.new(MauveTime.now, my_alert, &during).inside?
+ #
+ # ... or to ask when an alert will next be cued ...
+ #
+ # DuringRunner.new(MauveTime.now, my_alert, &during).find_next
+ #
+ # which will return a MauveTime object, or nil if the time period will
+ # not be valid again, at least not in the next week.
+ #
+ class DuringRunner
+
+ def initialize(time, alert=nil, &during)
+ raise ArgumentError.new("Must supply time (not #{time.inspect})") unless time.is_a?(Time)
+ @time = time
+ @alert = alert
+ @during = during || Proc.new { true }
+ @logger = Log4r::Logger.new "mauve::DuringRunner"
+ end
+
+ def now?
+ instance_eval(&@during)
+ end
+
+ def find_next(interval)
+ interval = 300 if true == interval.nil?
+ offset = (@time.nil?)? MauveTime.now : @time
+ plus_one_week = MauveTime.now + 604800 # ish
+ while offset < plus_one_week
+ offset += interval
+ if DuringRunner.new(offset, @alert, &@during).now?
+ @logger.debug("Found reminder time of #{offset}")
+ return offset
+ end
+ end
+ @logger.info("Could not find a reminder time less than a week "+
+ "for #{@alert}.")
+ nil # never again
+ end
+
+ protected
+ def hours_in_day(*hours)
+ x_in_list_of_y(@time.hour, hours.flatten)
+ end
+
+ def days_in_week(*days)
+ x_in_list_of_y(@time.wday, days.flatten)
+ end
+
+ ## Return true if the alert has not been acknowledged within a certain time.
+ #
+ def unacknowledged(seconds)
+ @alert &&
+ @alert.raised? &&
+ !@alert.acknowledged? &&
+ (@time - @alert.raised_at.to_time) > seconds
+ end
+
+ def x_in_list_of_y(x,y)
+ y.any? do |range|
+ if range.respond_to?("include?")
+ range.include?(x)
+ else
+ range == x
+ end
+ end
+ end
+
+ def working_hours?
+ now = (@time || MauveTime.now)
+ (8..17).include?(now.hour) and (1..5).include?(now.wday)
+ end
+
+ # Return true in the dead zone between 3 and 7 in the morning.
+ #
+ # Nota bene that this is used with different times in the reminder section.
+ #
+ # @return [Boolean] Whether now is a in the dead zone or not.
+ def dead_zone?
+ now = (@time || MauveTime.now)
+ (3..6).include?(now.hour)
+ end
+
+ end
+
+ # A Notification is an instruction to notify a list of people, at a
+ # particular alert level, on a periodic basis, and optionally under
+ # certain conditions specified by a block of code.
+ #
+ class Notification < Struct.new(:people, :level, :every, :during, :list)
+
+ def to_s
+ "#<Notification:of #{people.map { |p| p.username }.join(',')} at level #{level} every #{every}>"
+ end
+
+ attr_reader :thread_list
+
+ def initialize(people, level)
+
+ self.level = level
+ self.every = 300
+ self.people = people
+ end
+
+ def logger ; Log4r::Logger.new self.class.to_s ; end
+
+
+ # Updated code, now takes account of lists of people.
+ #
+ # @TODO refactor so we can test this more easily.
+ #
+ # @TODO Make sure that if no notifications is send at all, we log this
+ # as an error so that an email is send to the developers. Hum, we
+ # could have person.alert_changed return true if a notification was
+ # send (false otherwise) and add it to a queue. Then, dequeue till
+ # we see a "true" and abort. However, this needs a timeout loop
+ # around it and we will slow down the whole notificatin since it
+ # will have to wait untill such a time as it gets a true or timeout.
+ # Not ideal. A quick fix is to make sure that the clause in the
+ # configuration has a fall back that will send an alert in all cases.
+ #
+ def alert_changed(alert)
+
+ # Should we notificy at all?
+ is_relevant = DuringRunner.new(MauveTime.now, alert, &during).now?
+
+ to_notify = people.collect do |person|
+ case person
+ when Person
+ person
+ when PeopleList
+ person.people
+ else
+ logger.warn "Unable to notify #{person} (unrecognised class #{person.class})"
+ []
+ end
+ end.flatten.uniq.each do |person|
+ person.alert_changed(level, alert, is_relevant, remind_at_next(alert))
+ end
+ end
+
+ def remind_at_next(alert)
+ return nil unless alert.raised?
+ DuringRunner.new(MauveTime.now, alert, &during).find_next(every)
+ end
+
+ end
+
+end
diff --git a/lib/mauve/notifier.rb b/lib/mauve/notifier.rb
new file mode 100644
index 0000000..e0692f6
--- /dev/null
+++ b/lib/mauve/notifier.rb
@@ -0,0 +1,66 @@
+require 'mauve/mauve_thread'
+require 'mauve/notifiers'
+require 'mauve/notifiers/xmpp'
+
+module Mauve
+
+ class Notifier < MauveThread
+
+ DEFAULT_XMPP_MESSAGE = "Mauve server started."
+
+ include Singleton
+
+ attr_accessor :buffer, :sleep_interval
+
+ def initialize
+ @buffer = Queue.new
+ end
+
+ def main_loop
+
+ #
+ # Cycle through the buffer.
+ #
+ sz = @buffer.size
+
+ logger.debug("Notifier buffer is #{sz} in length") if sz > 1
+
+ (sz > 10 ? 10 : sz).times do
+ person, level, alert = @buffer.pop
+ begin
+ person.do_send_alert(level, alert)
+ rescue StandardError => ex
+ logger.debug ex.to_s
+ logger.debug ex.backtrace.join("\n")
+ end
+ end
+
+ end
+
+ def start
+ super
+
+ Configuration.current.notification_methods['xmpp'].connect if Configuration.current.notification_methods['xmpp']
+ end
+
+ def stop
+ Configuration.current.notification_methods['xmpp'].close
+
+ super
+ end
+
+ class << self
+
+ def enq(a)
+ instance.buffer.enq(a)
+ end
+
+ alias push enq
+
+ end
+
+ end
+
+end
+
+
diff --git a/lib/mauve/notifiers.rb b/lib/mauve/notifiers.rb
new file mode 100644
index 0000000..7ba1d71
--- /dev/null
+++ b/lib/mauve/notifiers.rb
@@ -0,0 +1,6 @@
+# encoding: UTF-8
+require 'mauve/notifiers/email'
+require 'mauve/notifiers/sms_default'
+require 'mauve/notifiers/sms_aql'
+require 'mauve/notifiers/xmpp'
+
diff --git a/lib/mauve/notifiers/debug.rb b/lib/mauve/notifiers/debug.rb
new file mode 100644
index 0000000..889a428
--- /dev/null
+++ b/lib/mauve/notifiers/debug.rb
@@ -0,0 +1,68 @@
+require 'fileutils'
+
+module Mauve
+ module Notifiers
+ #
+ # The Debug module adds two extra parameters to a notification method
+ # for debugging and testing.
+ #
+ module Debug
+ class << self
+ def included(base)
+ base.class_eval do
+ alias_method :send_alert_without_debug, :send_alert
+ alias_method :send_alert, :send_alert_to_debug_channels
+
+ # Specifying deliver_to_file allows the administrator to ask for alerts
+ # to be delivered to a particular file, which is assumed to be perused
+ # by a person rather than a machine.
+ #
+ attr :deliver_to_file, true
+
+ # Specifying deliver_to_queue allows a tester to ask for the send_alert
+ # parameters to be appended to a Queue object (or anything else that
+ # responds to <<).
+ #
+ attr :deliver_to_queue, true
+ end
+ end
+ end
+
+ def disable_normal_delivery!
+ @disable_normal_delivery = true
+ end
+
+ def send_alert_to_debug_channels(destination, alert, all_alerts, conditions = nil)
+ message = if respond_to?(:prepare_message)
+ prepare_message(destination, alert, all_alerts, conditions)
+ else
+ [destination, alert, all_alerts].inspect
+ end
+
+ if deliver_to_file
+ #lock_file = "#{deliver_to_file}.lock"
+ #while File.exists?(lock_file)
+ # sleep 0.1
+ #end
+ #FileUtils.touch(lock_file)
+ File.open("#{deliver_to_file}", "a+") do |fh|
+ fh.flock(File::LOCK_EX)
+ fh.print("#{MauveTime.now} from #{self.class}: " + message + "\n")
+ fh.flush()
+ end
+ #FileUtils.rm(lock_file)
+ end
+
+ deliver_to_queue << [destination, alert, all_alerts, conditions] if deliver_to_queue
+
+ if @disable_normal_delivery
+ true # pretend it happened OK if we're just testing
+ else
+ send_alert_without_debug(destination, alert, all_alerts, conditions)
+ end
+ end
+
+ end
+ end
+end
+
diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb
new file mode 100644
index 0000000..c445e09
--- /dev/null
+++ b/lib/mauve/notifiers/email.rb
@@ -0,0 +1,138 @@
+require 'time'
+require 'net/smtp'
+require 'rmail'
+require 'mauve/notifiers/debug'
+
+module Mauve
+ module Notifiers
+ module Email
+
+
+ class Default
+ attr_reader :name
+ attr :server, true
+ attr :port, true
+ attr :username, true
+ attr :password, true
+ attr :login_method, true
+ attr :from, true
+ attr :subject_prefix, true
+ attr :email_suffix, true
+
+ def username=(username)
+ @login_method ||= :plain
+ @username = username
+ end
+
+ def initialize(name)
+ @name = name
+ @server = '127.0.0.1'
+ @port = 25
+ @username = nil
+ @password = nil
+ @login_method = nil
+ @from = "mauve@localhost"
+ @hostname = "localhost"
+ @signature = "This is an automatic mailing, please do not reply."
+ @subject_prefix = ""
+ @suppressed_changed = nil
+ end
+
+ def send_alert(destination, alert, all_alerts, conditions = nil)
+ message = prepare_message(destination, alert, all_alerts, conditions)
+ args = [@server, @port]
+ args += [@username, @password, @login_method.to_sym] if @login_method
+ begin
+ Net::SMTP.start(*args) do |smtp|
+ smtp.send_message(message, @from, destination)
+ end
+ rescue Errno::ECONNREFUSED => e
+ @logger = Log4r::Logger.new "mauve::email_send_alert"
+ @logger.error("#{e.class}: #{e.message} raised. " +
+ "args = #{args.inspect} "
+ )
+ raise e
+ rescue => e
+ raise e
+ end
+ end
+
+ protected
+
+ def prepare_message(destination, alert, all_alerts, conditions = nil)
+ if conditions
+ @suppressed_changed = conditions[:suppressed_changed]
+ end
+
+ other_alerts = all_alerts - [alert]
+
+ m = RMail::Message.new
+
+ m.header.subject = subject_prefix +
+ case @suppressed_changed
+ when true
+ "Suppressing notifications (#{all_alerts.length} total)"
+
+ else
+ alert.summary_one_line.to_s
+ end
+ m.header.to = destination
+ m.header.from = @from
+ m.header.date = MauveTime.now
+
+ summary_formatted = ""
+# summary_formatted = " * "+alert.summary_two_lines.join("\n ")
+
+ case alert.update_type.to_sym
+ when :cleared
+ m.body = "An alert has been cleared:\n"+summary_formatted+"\n\n"
+ when :raised
+ m.body = "An alert has been raised:\n"+summary_formatted+"\n\n"
+ when :acknowledged
+ m.body = "An alert has been acknowledged by #{alert.acknowledged_by}:\n"+summary_formatted+"\n\n"
+ when :changed
+ m.body = "An alert has changed in nature:\n"+summary_formatted+"\n\n"
+ else
+ raise ArgumentError.new("Unknown update_type #{alert.update_type}")
+ end
+
+ # FIXME: include alert.detail as multipart mime
+ ##Thread.abort_on_exception = true
+ m.body += "\n" + '-'*10 + " This is the detail field " + '-'*44 + "\n\n"
+ #m.body += alert.get_details()
+ #m.body += alert.get_details_plain_text()
+ m.body += "\n" + '-'*80 + "\n\n"
+
+ if @suppressed_changed == true
+ m.body += <<-END
+IMPORTANT: I've been configured to suppress notification of individual changes
+to alerts until their rate decreases. If you still need notification of evrey
+single alert, you must watch the web front-end instead.
+
+ END
+ elsif @suppressed_changed == false
+ m.body += "(Notifications have slowed down - you will now be notified of every change)\n\n"
+ end
+
+ if other_alerts.empty?
+ m.body += (alert.update_type == :cleared ? "That was" : "This is")+
+ " currently the only alert outstanding\n\n"
+ else
+ m.body += other_alerts.length == 1 ?
+ "There is currently one other alert outstanding:\n\n" :
+ "There are currently #{other_alerts.length} other alerts outstanding:\n\n"
+
+# other_alerts.each do |other|
+# m.body += " * "+other.summary_two_lines.join("\n ")+"\n\n"
+# end
+ end
+
+ m.body += @email_suffix
+
+ m.to_s
+ end
+ include Debug
+ end
+ end
+ end
+end
diff --git a/lib/mauve/notifiers/sms_aql.rb b/lib/mauve/notifiers/sms_aql.rb
new file mode 100644
index 0000000..1cf8c04
--- /dev/null
+++ b/lib/mauve/notifiers/sms_aql.rb
@@ -0,0 +1,90 @@
+require 'mauve/notifiers/debug'
+require 'cgi'
+
+module Mauve
+ module Notifiers
+ module Sms
+
+ require 'net/https'
+ class AQL
+ GATEWAY = "https://gw1.aql.com/sms/sms_gw.php"
+
+ attr :username, true
+ attr :password, true
+ attr :from, true
+ attr :max_messages_per_alert, true
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def send_alert(destination, alert, all_alerts, conditions = nil)
+ uri = URI.parse(GATEWAY)
+
+ opts_string = {
+ :username => @username,
+ :password => @password,
+ :destination => normalize_number(destination),
+ :message => prepare_message(destination, alert, all_alerts, conditions),
+ :originator => @from,
+ :flash => @flash ? 1 : 0
+ }.map { |k,v| "#{k}=#{CGI::escape(v.to_s)}" }.join("&")
+
+ http = Net::HTTP.new(uri.host, uri.port)
+ if uri.port == 443
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ response, data = http.post(uri.path, opts_string, {
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ 'Content-Length' => opts_string.length.to_s
+ })
+
+ raise response unless response.kind_of?(Net::HTTPSuccess)
+ end
+
+ protected
+ def prepare_message(destination, alert, all_alerts, conditions=nil)
+ if conditions
+ @suppressed_changed = conditions[:suppressed_changed]
+ end
+
+ txt = case @suppressed_changed
+ when true then "TOO MUCH NOISE! Last notification: "
+ when false then "BACK TO NORMAL: "
+ else
+ ""
+ end
+
+ txt += "#{alert.update_type.upcase}: "
+ txt += alert.summary_one_line
+
+ others = all_alerts-[alert]
+ if !others.empty?
+ txt += (1 == others.length)?
+ "and a lone other." :
+ "and #{others.length} others."
+ #txt += "and #{others.length} others: "
+ #txt += others.map { |alert| alert.summary_one_line }.join(", ")
+ end
+
+ txt += "link: https://alert.bytemark.co.uk/alerts"
+
+ ## @TODO: Add a link to acknowledge the alert in the text?
+ #txt += "Acknoweledge alert: "+
+ # "https://alert.bytemark.co.uk/alert/acknowledge/"+
+ # "#{alert.id}/#{alert.get_default_acknowledge_time}
+
+ txt
+ end
+
+ def normalize_number(n)
+ n.split("").select { |s| (?0..?9).include?(s[0]) }.join.gsub(/^0/, "44")
+ end
+ include Debug
+ end
+ end
+ end
+end
+
diff --git a/lib/mauve/notifiers/sms_default.rb b/lib/mauve/notifiers/sms_default.rb
new file mode 100644
index 0000000..5afeedd
--- /dev/null
+++ b/lib/mauve/notifiers/sms_default.rb
@@ -0,0 +1,12 @@
+module Mauve
+ module Notifiers
+ module Sms
+ class Default
+ def initialize(*args)
+ raise ArgumentError.new("No default SMS provider, you must use the provider command to select one")
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/mauve/notifiers/templates/email.html.erb b/lib/mauve/notifiers/templates/email.html.erb
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/mauve/notifiers/templates/email.html.erb
diff --git a/lib/mauve/notifiers/templates/email.txt.erb b/lib/mauve/notifiers/templates/email.txt.erb
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/mauve/notifiers/templates/email.txt.erb
diff --git a/lib/mauve/notifiers/templates/sms.txt.erb b/lib/mauve/notifiers/templates/sms.txt.erb
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/mauve/notifiers/templates/sms.txt.erb
diff --git a/lib/mauve/notifiers/templates/xmpp.html.erb b/lib/mauve/notifiers/templates/xmpp.html.erb
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/mauve/notifiers/templates/xmpp.html.erb
diff --git a/lib/mauve/notifiers/templates/xmpp.txt.erb b/lib/mauve/notifiers/templates/xmpp.txt.erb
new file mode 100644
index 0000000..881c197
--- /dev/null
+++ b/lib/mauve/notifiers/templates/xmpp.txt.erb
@@ -0,0 +1 @@
+<%=arse %>
diff --git a/lib/mauve/notifiers/xmpp-smack.rb b/lib/mauve/notifiers/xmpp-smack.rb
new file mode 100644
index 0000000..a160a35
--- /dev/null
+++ b/lib/mauve/notifiers/xmpp-smack.rb
@@ -0,0 +1,395 @@
+# encoding: utf-8
+
+# Ruby.
+require 'pp'
+require 'log4r'
+require 'monitor'
+
+# Java. Note that paths are mangeled in jmauve_starter.
+require 'java'
+require 'smack.jar'
+require 'smackx.jar'
+include_class "org.jivesoftware.smack.XMPPConnection"
+include_class "org.jivesoftware.smackx.muc.MultiUserChat"
+include_class "org.jivesoftware.smack.RosterListener"
+
+module Mauve
+
+ module Notifiers
+
+ module Xmpp
+
+ class XMPPSmackException < StandardError
+ end
+
+ ## Main wrapper to smack java library.
+ #
+ # @author Yann Golanski
+ # @see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/
+ #
+ # This is a singleton which is not idea but works well for mauve's
+ # configuration file set up.
+ #
+ # In general, this class is meant to be intialized then the method
+ # create_slave_thread must be called. The latter will spawn a new
+ # thread that will do the connecting and sending of messages to
+ # the XMPP server. Once this is done, messages can be send via the
+ # send_msg() method. Those will be queued and depending on the load,
+ # should be send quickly enough. This is done so that the main thread
+ # can not worry about sending messages and can do important work.
+ #
+ # @example
+ # bot = Mauve::Notifiers::Xmpp::XMPPSmack.new()
+ # bot.run_slave_thread("chat.bytemark.co.uk", 'mauvealert', 'TopSecret')
+ # msg = "What fresh hell is this? -- Dorothy Parker."
+ # bot.send_msg("yann@chat.bytemark.co.uk", msg)
+ # bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg)
+ #
+ # @FIXME This won't quiet work with how mauve is set up.
+ #
+ class XMPPSmack
+
+ # Globals are evil.
+ @@instance = nil
+
+ # Default constructor.
+ #
+ # A queue (@queue) is used to pass information between master/slave.
+ def initialize ()
+ extend(MonitorMixin)
+ @logger = Log4r::Logger.new "mauve::XMPP_smack<#{Process.pid}>"
+ @queue = Queue.new
+ @xmpp = nil
+ @name = "mauve alert"
+ @slave_thread = nil
+ @regexp_muc = Regexp.compile(/^muc\:/)
+ @regexp_tail = Regexp.compile(/\/.*$/)
+ @jid_created_chat = Hash.new()
+ @separator = '<->'
+ @logger.info("Created XMPPSmack singleton")
+ end
+
+ # Returns the instance of the XMPPSmack singleton.
+ #
+ # @param [String] login The JID as a full address.
+ # @param [String] pwd The password corresponding to the JID.
+ # @return [XMPPSmack] The singleton instance.
+ def self.instance (login, pwd)
+ if true == @@instance.nil?
+ @@instance = XMPPSmack.new
+ jid, tmp = login.split(/@/)
+ srv, name = tmp.split(/\//)
+ name = "Mauve Alert Bot" if true == name.nil?
+ @@instance.run_slave_thread(srv, jid, pwd, name)
+ sleep 5 # FIXME: This really should be synced... But how?
+ end
+ return @@instance
+ end
+
+ # Create the thread that sends messages to the server.
+ #
+ # @param [String] srv The server address.
+ # @param [String] jid The JID.
+ # @param [String] pwd The password corresponding to the JID.
+ # @param [String] name The bot name.
+ # @return [NULL] nada
+ def run_slave_thread (srv, jid, pwd, name)
+ @srv = srv
+ @jid = jid
+ @pwd = pwd
+ @name = name
+ @logger.info("Creating slave thread on #{@jid}@#{@srv}/#{@name}.")
+ @slave_thread = Thread.new do
+ self.create_slave_thread()
+ end
+ return nil
+ end
+
+ # Returns whether instance is connected and authenticated.
+ #
+ # @return [Boolean] True or false.
+ def is_connected_and_authenticated? ()
+ return false if true == @xmpp.nil?
+ return (@xmpp.isConnected() and @xmpp.isAuthenticated())
+ end
+
+ # Creates the thread that does the actual sending to XMPP.
+ # @return [NULL] nada
+ def create_slave_thread ()
+ begin
+ @logger.info("Slave thread is now alive.")
+ self.open()
+ loop do
+ rcp, msg = @queue.deq().split(@separator, 2)
+ @logger.debug("New message for '#{rcp}' saying '#{msg}'.")
+ if rcp.match(@regexp_muc)
+ room = rcp.gsub(@regexp_muc, '').gsub(@regexp_tail, '')
+ self.send_to_muc(room, msg)
+ else
+ self.send_to_jid(rcp, msg)
+ end
+ end
+ rescue XMPPSmackException
+ @logger.fatal("Something is wrong")
+ ensure
+ @logger.info("XMPP bot disconnect.")
+ @xmpp.disconnect()
+ end
+ return nil
+ end
+
+ # Send a message to the recipient.
+ #
+ # @param [String] rcp The recipent MUC or JID.
+ # @param [String] msg The message.
+ # @return [NULL] nada
+ def send_msg(rcp, msg)
+ #if @slave_thread.nil? or not self.is_connected_and_authenticated?()
+ # str = "There is either no slave thread running or a disconnect..."
+ # @logger.warn(str)
+ # self.reconnect()
+ #end
+ @queue.enq(rcp + @separator + msg)
+ return nil
+ end
+
+ # Sends a message to a room.
+ #
+ # @param [String] room The name of the room.
+ # @param [String] mgs The message to send.
+ # @return [NULL] nada
+ def send_to_muc (room, msg)
+ if not @jid_created_chat.has_key?(room)
+ @jid_created_chat[room] = MultiUserChat.new(@xmpp, room)
+ @jid_created_chat[room].join(@name)
+ end
+ @logger.debug("Sending to MUC '#{room}' message '#{msg}'.")
+ @jid_created_chat[room].sendMessage(msg)
+ return nil
+ end
+
+ # Sends a message to a jid.
+ #
+ # Do not destroy the chat, we can reuse it when the user log back in again.
+ # Maybe?
+ #
+ # @param [String] jid The JID of the recipient.
+ # @param [String] mgs The message to send.
+ # @return [NULL] nada
+ def send_to_jid (jid, msg)
+ if true == jid_is_available?(jid)
+ if not @jid_created_chat.has_key?(jid)
+ @jid_created_chat[jid] = @xmpp.getChatManager.createChat(jid, nil)
+ end
+ @logger.debug("Sending to JID '#{jid}' message '#{msg}'.")
+ @jid_created_chat[jid].sendMessage(msg)
+ end
+ return nil
+ end
+
+ # Check to see if the jid is available or not.
+ #
+ # @param [String] jid The JID of the recipient.
+ # @return [Boolean] Whether we can send a message or not.
+ def jid_is_available?(jid)
+ if true == @xmpp.getRoster().getPresence(jid).isAvailable()
+ @logger.debug("#{jid} is available. Status is " +
+ "#{@xmpp.getRoster().getPresence(jid).getStatus()}")
+ return true
+ else
+ @logger.warn("#{jid} is not available. Status is " +
+ "#{@xmpp.getRoster().getPresence(jid).getStatus()}")
+ return false
+ end
+ end
+
+ # Opens a connection to the xmpp server at given port.
+ #
+ # @return [NULL] nada
+ def open()
+ @logger.info("XMPP bot is being created.")
+ self.open_connection()
+ self.open_authentication()
+ self.create_roster()
+ sleep 5
+ return nil
+ end
+
+ # Connect to server.
+ #
+ # @return [NULL] nada
+ def open_connection()
+ @xmpp = XMPPConnection.new(@srv)
+ if false == self.connect()
+ str = "Connection refused"
+ @logger.error(str)
+ raise XMPPSmackException.new(str)
+ end
+ @logger.debug("XMPP bot connected successfully.")
+ return nil
+ end
+
+ # Authenticat connection.
+ #
+ # @return [NULL] nada
+ def open_authentication()
+ if false == self.login(@jid, @pwd)
+ str = "Authentication failed"
+ @logger.error(str)
+ raise XMPPSmackException.new(str)
+ end
+ @logger.debug("XMPP bot authenticated successfully.")
+ return nil
+ end
+
+ # Create a new roster and listener.
+ #
+ # @return [NULL] nada
+ def create_roster
+ @xmpp.getRoster().addRosterListener(RosterListener.new())
+ @xmpp.getRoster().reload()
+ @xmpp.getRoster().getPresence(@xmpp.getUser).setStatus(
+ "Purple alert! Purple alert!")
+ @logger.debug("XMPP bot roster aquired successfully.")
+ return nil
+ end
+
+ # Connects to the server.
+ #
+ # @return [Boolean] true (aka sucess) or false (aka failure).
+ def connect ()
+ @xmpp.connect()
+ return @xmpp.isConnected()
+ end
+
+ # Login onto the server.
+ #
+ # @param [String] jid The JID.
+ # @param [String] pwd The password corresponding to the JID.
+ # @return [Boolean] true (aka sucess) or false (aka failure).
+ def login (jid, pwd)
+ @xmpp.login(jid, pwd, @name)
+ return @xmpp.isAuthenticated()
+ end
+
+ # Reconnects in case of errors.
+ #
+ # @return [NULL] nada
+ def reconnect()
+ @xmpp.disconnect
+ @slave_thread = Thread.new do
+ self.create_slave_thread()
+ end
+ return nil
+ end
+
+ def presenceChanged ()
+ end
+
+ end # XMPPSmack
+
+
+ ## This is the class that gets called in person.rb.
+ #
+ # This class is a wrapper to XMPPSmack which does the hard work. It is
+ # done this way to conform to the mauve configuration file way of
+ # defining notifications.
+ #
+ # @author Yann Golanski
+ class Default
+
+ # Name of the class.
+ attr_reader :name
+
+ # Atrtribute.
+ attr_accessor :jid
+
+ # Atrtribute.
+ attr_accessor :password
+
+ # Atrtribute.
+ attr_accessor :initial_jid
+
+ # Atrtribute.
+ attr_accessor :initial_messages
+
+ # Default constructor.
+ #
+ # @param [String] name The name of the notifier.
+ def initialize (name)
+ extend(MonitorMixin)
+ @name = name
+ @logger = Log4r::Logger.new "mauve::XMPP_default<#{Process.pid}>"
+ end
+
+ # Sends a message to the relevant jid or muc.
+ #
+ # We have no way to know if a messages was recieved, only that
+ # we send it.
+ #
+ # @param [String] destionation
+ # @param [Alert] alert A mauve alert class
+ # @param [Array] all_alerts subset of current alerts
+ # @param [Hash] conditions Supported conditions, see above.
+ # @return [Boolean] Whether a message can be send or not.
+ def send_alert(destination, alert, all_alerts, conditions = nil)
+ synchronize {
+ client = XMPPSmack.instance(@jid, @password)
+ if not destination.match(/^muc:/)
+ if false == client.jid_is_available?(destination.gsub(/^muc:/, ''))
+ return false
+ end
+ end
+ client.send_msg(destination, convert_alert_to_message(alert))
+ return true
+ }
+ end
+
+ # Takes an alert and converts it into a message.
+ #
+ # @param [Alert] alert The alert to convert.
+ # @return [String] The message, either as HTML.
+ def convert_alert_to_message(alert)
+ arr = alert.summary_three_lines
+ str = arr[0] + ": " + arr[1]
+ str += " -- " + arr[2] if false == arr[2].nil?
+ str += "."
+ return str
+ #return alert.summary_two_lines.join(" -- ")
+ #return "<p>" + alert.summary_two_lines.join("<br />") + "</p>"
+ end
+
+ # This is so unit tests can run fine.
+ include Debug
+
+ end # Default
+
+ end
+ end
+end
+
+# This is a simple example of usage. Run with:
+# ../../../jmauve_starter.rb xmpp-smack.rb
+# Clearly, the mauve jabber password is not correct.
+#
+# /!\ WARNING: DO NOT COMMIT THE REAL PASSWORD TO MERCURIAL!!!
+#
+def send_msg()
+ bot = Mauve::Notifiers::Xmpp::XMPPSmack.instance(
+ "mauvealert@chat.bytemark.co.uk/testing1234", '')
+ msg = "What fresh hell is this? -- Dorothy Parker."
+ bot.send_msg("yann@chat.bytemark.co.uk", msg)
+ bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg)
+ sleep 2
+end
+
+if __FILE__ == './'+$0
+ Thread.abort_on_exception = true
+ logger = Log4r::Logger.new('mauve')
+ logger.level = Log4r::DEBUG
+ logger.add Log4r::Outputter.stdout
+ send_msg()
+ send_msg()
+ logger.info("START")
+ logger.info("END")
+end
diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb
new file mode 100644
index 0000000..d216788
--- /dev/null
+++ b/lib/mauve/notifiers/xmpp.rb
@@ -0,0 +1,296 @@
+require 'log4r'
+require 'xmpp4r'
+require 'xmpp4r/xhtml'
+require 'xmpp4r/roster'
+require 'xmpp4r/muc/helper/simplemucclient'
+require 'mauve/notifiers/debug'
+#Jabber::debug = true
+
+module Mauve
+ module Notifiers
+ module Xmpp
+
+ class CountingMUCClient < Jabber::MUC::SimpleMUCClient
+
+ attr_reader :participants
+
+ def initialize(*a)
+ super(*a)
+ @participants = 0
+ self.on_join { @participants += 1 }
+ self.on_leave { @participants -= 1 }
+ end
+
+ end
+
+ class Default
+
+ include Jabber
+
+ # Atrtribute.
+ attr_reader :name
+
+ # Atrtribute.
+ attr_accessor :jid, :password
+
+ # Atrtribute.
+ attr_accessor :initial_jid
+
+ # Atrtribute.
+ attr_accessor :initial_messages
+
+ def initialize(name)
+ @name = name
+ @mucs = {}
+ @roster = nil
+ end
+
+ def logger
+ @logger ||= Log4r::Logger.new self.class.to_s
+ end
+
+ def reconnect
+ if @client
+ begin
+ logger.debug "Jabber closing old client connection"
+ @client.close
+ @client = nil
+ @roster = nil
+ rescue Exception => ex
+ logger.error "#{ex} when reconnecting"
+ end
+ end
+
+ logger.debug "Jabber starting connection to #{@jid}"
+ @client = Client.new(JID::new(@jid))
+ @client.connect
+ logger.debug "Jabber authentication"
+
+ @client.auth_nonsasl(@password, false)
+ @roster = Roster::Helper.new(@client)
+
+ # Unconditionally accept all roster add requests, and respond with a
+ # roster add + subscription request of our own if we're not subscribed
+ # already
+ @roster.add_subscription_request_callback do |ri, stanza|
+ Thread.new do
+ logger.debug("Accepting subscription request from #{stanza.from}")
+ @roster.accept_subscription(stanza.from)
+ ensure_roster_and_subscription!(stanza.from)
+ end.join
+ end
+
+ @roster.wait_for_roster
+ logger.debug "Jabber authenticated, setting presence"
+
+ @client.send(Presence.new.set_type(:available))
+ @mucs = {}
+
+ logger.debug "Jabber is ready in theory"
+ end
+
+ def reconnect_and_retry_on_error
+ @already_reconnected = false
+ begin
+ yield
+ rescue StandardError => ex
+ logger.error "#{ex} during notification\n"
+ logger.debug ex.backtrace
+ if !@already_reconnected
+ reconnect
+ @already_reconnected = true
+ retry
+ else
+ raise ex
+ end
+ end
+ end
+
+ def connect
+ self.reconnect_and_retry_on_error { self.send_msg(@initial_jid, "Hello!") }
+ end
+
+ def close
+ self.send_msg(@initial_jid, "Goodbye!")
+ @client.close
+ end
+
+ # Takes an alert and converts it into a message.
+ #
+ # @param [Alert] alert The alert to convert.
+ # @return [String] The message, either as HTML.
+ def convert_alert_to_message(alert)
+ arr = alert.summary_three_lines
+ str = arr[0] + ": " + arr[1]
+ str += " -- " + arr[2] if false == arr[2].nil?
+ str += "."
+ return str
+ #return alert.summary_two_lines.join(" -- ")
+ #return "<p>" + alert.summary_two_lines.join("<br />") + "</p>"
+ end
+
+ # Attempt to send an alert using XMPP.
+ # +destination+ is the JID you're sending the alert to. This should be
+ # a bare JID in the case of an individual, or muc:<room>@<server> for
+ # chatrooms (XEP0045). The +alert+ object is turned into a pretty
+ # message and sent to the destination as a message, if the +conditions+
+ # are met. all_alerts are currently ignored.
+ #
+ # The only suported condition at the moment is :if_presence => [choices]
+ # which checks whether the jid in question has a presence matching one
+ # or more of the choices - see +check_jid_has_presence+ for options.
+
+ def send_alert(destination, alert, all_alerts, conditions = nil)
+ #message = Message.new(nil, alert.summary_two_lines.join("\n"))
+ message = Message.new(nil, convert_alert_to_message(alert))
+
+ if conditions
+ @suppressed_changed = conditions[:suppressed_changed]
+ end
+
+ # MUC JIDs are prefixed with muc: - we need to strip this out.
+ destination_is_muc, dest_jid = self.is_muc?(destination)
+
+ begin
+ xhtml = XHTML::HTML.new("<p>" +
+ convert_alert_to_message(alert)+
+# alert.summary_three_lines.join("<br />") +
+ #alert.summary_two_lines.join("<br />") +
+ "</p>")
+ message.add_element(xhtml)
+ rescue REXML::ParseException => ex
+ logger.warn("Can't send XMPP alert as valid XHTML-IM, falling back to plaintext")
+ logger.debug(ex)
+ end
+
+ logger.debug "Jabber sending #{message} to #{destination}"
+ reconnect unless @client
+
+ ensure_roster_and_subscription!(dest_jid) unless destination_is_muc
+
+ if conditions && !check_alert_conditions(dest_jid, conditions)
+ logger.debug("Alert conditions not met, not sending XMPP alert to #{jid}")
+ return false
+ end
+
+ if destination_is_muc
+ if !@mucs[dest_jid]
+ @mucs[dest_jid] = CountingMUCClient.new(@client)
+ @mucs[dest_jid].join(JID.new(dest_jid))
+ end
+ reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true }
+ else
+ message.to = dest_jid
+ reconnect_and_retry_on_error { @client.send(message) ; true }
+ end
+ end
+
+ # Sends a message to the destionation.
+ #
+ # @param [String] destionation The (full) JID to send to.
+ # @param [String] msg The (formatted) message to send.
+ # @return [NIL] nada.
+ def send_msg(destination, msg)
+ reconnect unless @client
+ message = Message.new(nil, msg)
+ destination_is_muc, dest_jid = self.is_muc?(destination)
+ if destination_is_muc
+ if !@mucs[dest_jid]
+ @mucs[dest_jid] = CountingMUCClient.new(@client)
+ @mucs[dest_jid].join(JID.new(dest_jid))
+ end
+ reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true }
+ else
+ message.to = dest_jid
+ reconnect_and_retry_on_error { @client.send(message) ; true }
+ end
+ return nil
+ end
+
+ protected
+
+ # Checks whether the destination JID is a MUC.
+ # Returns [true/false, destination]
+ def is_muc?(destination)
+ if /^muc:(.*)/.match(destination)
+ [true, $1]
+ else
+ [false, destination]
+ end
+ end
+
+ # Checks to see if the JID is in our roster, and whether we are
+ # subscribed to it or not. Will add to the roster and subscribe as
+ # is necessary to ensure both are true.
+ def ensure_roster_and_subscription!(jid)
+ jid = JID.new(jid)
+ ri = @roster.find(jid)[jid]
+ if ri.nil?
+ @roster.add(jid, nil, true)
+ else
+ ri.subscribe unless [:to, :both, :remove].include?(ri.subscription)
+ end
+ rescue Exception => ex
+ logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}")
+ end
+
+ def check_alert_conditions(destination, conditions)
+ any_failed = conditions.keys.collect do |key|
+ case key
+ when :if_presence : check_jid_has_presence(destination, conditions[:if_presence])
+ else
+ #raise ArgumentError.new("Unknown alert condition, #{key} => #{conditions[key]}")
+ # FIXME - clean up this use of :conditions to pass arbitrary
+ # parameters to notifiers; for now we need to ignore this.
+ true
+ end
+ end.include?(false)
+ !any_failed
+ end
+
+ # Checks our roster to see whether the jid has a resource with at least
+ # one of the included presences. Acceptable +presence+ types and their
+ # meanings for individuals:
+ #
+ # :online, :offline - user is logged in or out
+ # :available - jabber status is nil (available) or chat
+ # :unavailable - - jabber status is away, dnd or xa
+ # :unknown - don't know (not in roster)
+ #
+ # For MUCs: TODO
+ # Returns true if at least one of the presence specifiers for the jid
+ # is met, false otherwise. Note that if the alerter can't see the alertee's
+ # presence, only 'unknown' will match - generally, you'll want [:online, :unknown]
+ def check_jid_has_presence(jid, presence_or_presences)
+ return true if jid.match(/^muc:/)
+
+ reconnect unless @client
+
+ presences = [presence_or_presences].flatten
+ roster_item = @roster.find(jid)
+ roster_item = roster_item[roster_item.keys[0]]
+ resource_presences = []
+ roster_item.each_presence {|p| resource_presences << p.show } if roster_item
+
+ results = presences.collect do |need_presence|
+ case need_presence
+ when :online : (roster_item && [:to, :both].include?(roster_item.subscription) && roster_item.online?)
+ when :offline : (roster_item && [:to, :both].include?(roster_item.subscription) && !roster_item.online?)
+ when :available : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences.include?(nil) ||
+ resource_presences.include?(:chat)))
+ # No resources are nil or chat
+ when :unavailable : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences - [:away, :dnd, :xa]).empty?)
+ # Not in roster or don't know subscription
+ when :unknown : (roster_item.nil? || [:none, :from].include?(roster_item.subscription))
+ else
+ raise ArgumentError.new("Unknown presence possibility: #{need_presence}")
+ end
+ end
+ results.include?(true)
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb
new file mode 100644
index 0000000..2e4c737
--- /dev/null
+++ b/lib/mauve/people_list.rb
@@ -0,0 +1,44 @@
+# encoding: UTF-8
+require 'log4r'
+require 'mauve/calendar_interface'
+
+module Mauve
+
+ # Stores a list of name.
+ #
+ # @author Yann Golanski
+ class PeopleList < Struct.new(:label, :list)
+
+ # Default contrustor.
+ def initialize (*args)
+ super(*args)
+ end
+
+ def label
+ self[:label]
+ end
+
+ alias username label
+
+ def list
+ self[:list]
+ end
+
+ #
+ # Set up the logger
+ def logger
+ @logger ||= Log4r::Logger.new self.class
+ end
+
+ #
+ # Return the array of people
+ #
+ def people
+ list.collect do |name|
+ Configuration.current.people.has_key?(name) ? Configuration.current.people[name] : nil
+ end.reject{|person| person.nil?}
+ end
+
+ end
+
+end
diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb
new file mode 100644
index 0000000..6e9fcb4
--- /dev/null
+++ b/lib/mauve/person.rb
@@ -0,0 +1,230 @@
+# encoding: UTF-8
+require 'timeout'
+require 'log4r'
+
+module Mauve
+ class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low)
+
+ attr_reader :notification_thresholds
+
+ def initialize(*args)
+ @notification_thresholds = { 60 => Array.new(10) }
+ @suppressed = false
+ super(*args)
+ end
+
+ def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end
+
+ def suppressed?
+ @suppressed
+ end
+
+ # This class implements an instance_eval context to execute the blocks
+ # for running a notification block for each person.
+ #
+ class NotificationCaller
+
+ def initialize(alert, other_alerts, notification_methods, base_conditions={})
+ logger = Log4r::Logger.new "mauve::NotificationCaller"
+ @alert = alert
+ @other_alerts = other_alerts
+ @notification_methods = notification_methods
+ @base_conditions = base_conditions
+ end
+
+ def method_missing(name, destination, *args)
+ conditions = @base_conditions.merge(args[0] ? args[0] : {})
+ notification_method = @notification_methods[name.to_s]
+
+ unless notification_method
+ raise NoMethodError.new("#{name} not defined as a notification method")
+ end
+ # Methods are expected to return true or false so the user can chain
+ # them together with || as fallbacks. So we have to catch exceptions
+ # and turn them into false.
+ #
+ notification_method.send_alert(destination, @alert, @other_alerts, conditions)
+ end
+
+ end
+
+ ## Deals with changes in an alert.
+ #
+ # == Old comments by Matthew.
+ #
+ # An AlertGroup tells a Person that an alert has changed. Within
+ # this alert group, the alert may or may not be "relevant" to this
+ # person, but it is ultimately up to the Person to decide whether to
+ # send a notification. (i.e. notification of acks/clears should
+ # always go out to a Person who was notified of the original alert,
+ # even if the alert is no longer relevant to them).
+ #
+ # == New comment
+ #
+ # The old code works like this: An alert arrives, with a relevance. An
+ # AlertChanged is created and the alert may or may not be send. The
+ # problem is that alerts can be relevant AFTER the initial raise and this
+ # code (due to AlertChange.was_relevant_when_raised?()) will ignore it.
+ # This is wrong.
+ #
+ #
+ # The Thread.exclusive wrapper around the AlertChanged creation makes
+ # sure that two AlertChanged are not created at the same time. This
+ # caused both instances to set the remind_at time of the other to nil.
+ # Thus reminders were never seen which is clearly wrong. This bug was
+ # only showing on jruby due to green threads in MRI.
+ #
+ #
+ # @author Matthew Bloch, Yann Golanski
+ # @param [symb] level Level of the alert.
+ # @param [Alert] alert An alert object.
+ # @param [Boolean] Whether the alert is relevant as defined by notification
+ # class.
+ # @param [MauveTime] When to send remind.
+ # @return [NULL] nada
+ def alert_changed(level, alert, is_relevant=true, remind_at=nil)
+ # User should get notified but will not since on holiday.
+ str = String.new
+# if is_on_holiday?
+# is_relevant = false
+# str = ' (user on holiday)'
+# end
+
+ # Deals with AlertChange database entry.
+ last_change = AlertChanged.first(:alert_id => alert.id, :person => username)
+ if not last_change.nil?
+ if not last_change.remind_at.nil? and not remind_at.nil?
+ if last_change.remind_at.to_time < remind_at
+ remind_at = last_change.remind_at.to_time
+ end
+ end
+ end
+
+ new_change = AlertChanged.create(
+ :level => level.to_s,
+ :alert_id => alert.id,
+ :at => MauveTime.now,
+ :person => username,
+ :update_type => alert.update_type,
+ :remind_at => remind_at,
+ :was_relevant => is_relevant)
+
+ # We need to look at the AlertChanged objects to reset them to
+ # the right value. What is the right value? Well...
+ if true == is_relevant
+ last_change.was_relevant = true if false == last_change.nil?
+ end
+
+ # Send the notification is need be.
+ if !last_change || last_change.update_type.to_sym == :cleared
+ # Person has never heard of this alert before, or previously cleared.
+ #
+ # We don't send any alert if such a change isn't relevant to this
+ # Person at this time.
+ send_alert(level, alert) if is_relevant and [:raised, :changed].include?(alert.update_type.to_sym)
+
+ else
+ # Relevance is determined by whether the user heard of this alert
+ # being raised.
+ send_alert(level, alert) if last_change.was_relevant_when_raised?
+ end
+ end
+ end
+
+ def remind(alert, level)
+ logger.debug("Reminder for #{alert} send at level #{level}.")
+ send_alert(level, alert)
+ end
+
+ #
+ # This just wraps send_alert by sending the job to a queue.
+ #
+ def send_alert(level, alert)
+ Notifier.push([self, level, alert])
+ end
+
+ def do_send_alert(level, alert)
+ now = MauveTime.now
+ suppressed_changed = nil
+ threshold_breached = @notification_thresholds.any? do |period, previous_alert_times|
+ first = previous_alert_times.first
+ first.is_a?(MauveTime) and (now - first) < period
+ end
+
+ this_alert_suppressed = false
+
+ if Server.instance.started_at > alert.updated_at.to_time and (Server.instance.started_at + Server.instance.initial_sleep) > MauveTime.now
+ logger.warn("Alert last updated in prior run of mauve -- ignoring for initial sleep period.")
+ this_alert_suppressed = true
+ elsif threshold_breached
+ unless suppressed?
+ logger.warn("Suspending notifications to #{username} until further notice.")
+ suppressed_changed = true
+ end
+ @suppressed = true
+ else
+ if suppressed?
+ suppressed_changed = false
+ logger.warn "Starting to send notifications again for #{username}."
+ else
+ logger.info "Notifying #{username} of #{alert} at level #{level}"
+ end
+ @suppressed = false
+ end
+
+ return if suppressed? or this_alert_suppressed
+
+ result = NotificationCaller.new(
+ alert,
+ current_alerts,
+ Configuration.current.notification_methods,
+ :suppressed_changed => suppressed_changed
+ ).instance_eval(&__send__(level))
+
+ if result
+ #
+ # Remember that we've sent an alert
+ #
+ @notification_thresholds.each do |period, previous_alert_times|
+ @notification_thresholds[period].replace(previous_alert_times[1..period-1] + [now])
+ end
+
+ logger.info("Notification for #{username} of #{alert} at level #{level} has been successful")
+ else
+ logger.error("Failed to notify #{username} about #{alert} at level #{level}")
+ end
+ end
+
+ # Returns the subset of current alerts that are relevant to this Person.
+ #
+ def current_alerts
+ Alert.all_current.select do |alert|
+ my_last_update = AlertChanged.first(:person => username, :alert_id => alert.id)
+ my_last_update && my_last_update.update_type != :cleared
+ end
+ end
+
+ protected
+ # Remembers that an alert has been sent so that we can later check whether
+ # too many alerts have been sent in a particular period.
+ #
+ def remember_alert(now=MauveTime.now)
+ end
+
+ # Returns time period over which "too many" alerts have been sent, or nil
+ # if none.
+ #
+ def threshold_breached(now=MauveTime.now)
+ end
+
+ # Whether the person is on holiday or not.
+ #
+ # @return [Boolean] True if person on holiday, false otherwise.
+ def is_on_holiday? ()
+ return false if true == holiday_url.nil? or '' == holiday_url
+ return CalendarInterface.is_user_on_holiday?(holiday_url, username)
+ end
+
+ end
+
+end
diff --git a/lib/mauve/processor.rb b/lib/mauve/processor.rb
new file mode 100644
index 0000000..414f640
--- /dev/null
+++ b/lib/mauve/processor.rb
@@ -0,0 +1,109 @@
+# encoding: UTF-8
+
+require 'mauve/mauve_thread'
+
+module Mauve
+
+ class Processor < MauveThread
+
+ include Singleton
+
+ attr_accessor :buffer, :transmission_cache_expire_time, :sleep_interval
+
+ def initialize
+ # Buffer for UDP socket packets.
+ @buffer = Queue.new
+
+ # Set the logger up
+ @logger = Log4r::Logger.new(self.class.to_s)
+
+ #
+ # Set up the transmission id cache
+ #
+ @transmission_id_cache = {}
+ @transmission_cache_expire_time = 300
+ @sleep_interval = 1
+ end
+
+ def main_loop
+
+ sz = @buffer.size
+
+ return if sz == 0
+
+ Timer.instance.freeze
+
+ logger.info("Buffer has #{sz} packets waiting...")
+
+ triplets = []
+ #
+ # Only do the loop a maximum of 10 times every @sleep_interval seconds
+ #
+ (sz > 20 ? 20 : sz).times do
+ triplets += @buffer.deq
+ end
+
+ triplets.each do |data, client, received_at|
+
+ @logger.debug("Got #{data.inspect} from #{client.inspect}")
+
+ ip_source = "#{client[3]}:#{client[1]}"
+ update = Proto::AlertUpdate.new
+
+ begin
+ update.parse_from_string(data)
+
+ if @transmission_id_cache[update.transmission_id.to_s]
+ @logger.debug("Ignoring duplicate transmission id #{data.transmission_id}")
+ #
+ # Continue with next packet.
+ #
+ next
+ end
+
+ @logger.debug "Update #{update.transmission_id} sent at #{update.transmission_time} from "+
+ "'#{update.source}'@#{ip_source} alerts #{update.alert.length}"
+
+ Alert.receive_update(update, received_at)
+
+ rescue Protobuf::InvalidWireType,
+ NotImplementedError,
+ DataObjects::IntegrityError => ex
+
+ @logger.error "#{ex} (#{ex.class}) while parsing #{data.length} bytes "+
+ "starting '#{data[0..16].inspect}' from #{ip_source}"
+
+ @logger.debug ex.backtrace.join("\n")
+
+ ensure
+ @transmission_id_cache[update.transmission_id.to_s] = MauveTime.now
+
+ end
+ end
+ Timer.instance.thaw
+ end
+
+ def expire_transmission_id_cache
+ now = MauveTime.now
+ to_delete = []
+
+ @transmission_id_cache.each do |tid, received_at|
+ to_delete << tid if (now - received_at) > @transmission_cache_expire_time
+ end
+
+ to_delete.each do |tid|
+ @transmission_id_cache.delete(tid)
+ end
+ end
+
+ class << self
+
+ def enq(a)
+ instance.buffer.enq(a)
+ end
+
+ alias push enq
+
+ end
+ end
+end
diff --git a/lib/mauve/proto.rb b/lib/mauve/proto.rb
new file mode 100644
index 0000000..6c22609
--- /dev/null
+++ b/lib/mauve/proto.rb
@@ -0,0 +1,116 @@
+### Generated by rprotoc. DO NOT EDIT!
+### <proto file: mauve.proto>
+# package mauve.proto;
+#
+# // An alert is a notification of an event in your business, project or
+# // enterprise for which someone might want to stop what they're doing and
+# // attend to.
+# //
+# // Alerts
+# //
+# message Alert {
+# // Every separate alert must have a unique Id attached. When sending a
+# // repeated or altered alert, using the same alert id will overwrite
+# // the previous settings.
+# //
+# required string id = 1;
+#
+# // The UNIX time at which this alert was or will be raised. If set to zero it
+# // means 'this alert is assumed to be raised already'.
+# //
+# optional uint64 raise_time = 2;
+#
+# // The UNIX time at which this alert was or will be cleared. If set to zero
+# // it means 'do not clear automatically'. Messages with clear times set before
+# // alert times are not valid, and will be ignored.
+# //
+# optional uint64 clear_time = 3;
+#
+# // The subject is the name of the server/device/entity that is being alerted
+# // about. If not supplied, assumed to be the same as source.
+# //
+# optional string subject = 4;
+#
+# // The summary is a summary of an alert (100 characters or less) that
+# // can be fitted into a pager or SMS message, along with the source & subject.
+# //
+# optional string summary = 5;
+#
+# // The detail can be an arbitrary HTML fragment for display on suitable
+# // devices giving fuller information about the alert.
+# //
+# optional string detail = 6;
+#
+# // The importance of this alert (relative to others from this source). Zero
+# // is 'unspecified importance' which will use the server's default.
+# //
+# optional uint32 importance = 7;
+#
+# }
+#
+# // The AlertUpdate is the unit of communication from an alerting source;
+# // it consists of one or more alerts, which can either replace, or supplement
+# // the alert data for that source.
+# //
+# message AlertUpdate {
+# // Random number with each transmission, so that destinations can easily
+# // identify and discard duplicate transmissions that are inherent to the
+# // protocol.
+# //
+# required uint64 transmission_id = 1;
+#
+# // The source of an alert represents the sender - each possible sender
+# // should set this consistently (e.g. the name of the monitoring system
+# // that is generating a particular class of alerts).
+# //
+# required string source = 2;
+#
+# // When set to true, signals that this update should completely replace
+# // all current data for this source (so unlisted previous alerts are deemed
+# // to be cleared).
+# //
+# required bool replace = 3 [ default = false ];
+#
+# // Alert data follows
+# //
+# repeated Alert alert = 4;
+#
+# // Signature to authenticate this data - no scheme defined currently, maybe
+# // SHA1(alert.raw + password) ?
+# //
+# optional bytes signature = 5;
+#
+# // The UNIX time at which the packet was sent by the server.
+# //
+# optional uint64 transmission_time = 8;
+# }
+#
+
+require 'protobuf/message/message'
+require 'protobuf/message/enum'
+require 'protobuf/message/service'
+require 'protobuf/message/extend'
+
+module Mauve
+ module Proto
+ class Alert < ::Protobuf::Message
+ defined_in __FILE__
+ required :string, :id, 1
+ optional :uint64, :raise_time, 2
+ optional :uint64, :clear_time, 3
+ optional :string, :subject, 4
+ optional :string, :summary, 5
+ optional :string, :detail, 6
+ optional :uint32, :importance, 7
+ end
+ class AlertUpdate < ::Protobuf::Message
+ defined_in __FILE__
+ required :uint64, :transmission_id, 1
+ required :string, :source, 2
+ required :bool, :replace, 3, :default => false
+ repeated :Alert, :alert, 4
+ optional :bytes, :signature, 5
+ optional :uint64, :transmission_time, 8
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/mauve/sender.rb b/lib/mauve/sender.rb
new file mode 100644
index 0000000..f27efe5
--- /dev/null
+++ b/lib/mauve/sender.rb
@@ -0,0 +1,94 @@
+# encoding: UTF-8
+require 'ipaddr'
+require 'resolv'
+require 'socket'
+require 'mauve/mauve_time'
+
+module Mauve
+ class Sender
+ DEFAULT_PORT = 32741
+ include Resolv::DNS::Resource::IN
+
+ def initialize(*destinations)
+ destinations = destinations.flatten
+
+ destinations = begin
+ File.read("/etc/mauvealert/mauvesend.destination").split(/\s+/)
+ rescue Errno::ENOENT => notfound
+ []
+ end if destinations.empty?
+
+ if !destinations || destinations.empty?
+ raise ArgumentError.new("No destinations specified, and could not read any destinations from /etc/mauvealert/mauvesend.destination")
+ end
+
+ @destinations = destinations.map do |spec|
+ next_spec = begin
+ # FIXME: for IPv6
+ port = spec.split(":")[1] || DEFAULT_PORT
+ IPAddr.new(spec.split(":")[0])
+ ["#{spec}:#{port}"]
+ rescue ArgumentError => not_an_ip_address
+ Resolv::DNS.open do |dns|
+ srv_spec = spec[0] == ?_ ? spec : "_mauvealert._udp.#{spec}"
+ list = dns.getresources(srv_spec, SRV).map do |srv|
+ srv.target.to_s + ":#{srv.port}"
+ end
+ list = [spec] if list.empty?
+ list.map do |spec2|
+ spec2_addr, spec2_port = spec2.split(":")
+ spec2_port ||= DEFAULT_PORT
+ dns.getresources(spec2_addr, A).map do |a|
+ "#{a.address}:#{spec2_port}"
+ end
+ end
+ end
+ end.flatten
+
+ error "Can't resolve destination #{spec}" if next_spec.empty?
+
+ next_spec
+ end.
+ flatten.
+ uniq
+ end
+
+ def send(update, verbose=0)
+
+ #
+ # Must have a source, so default to hostname if user doesn't care
+ update.source ||= `hostname -f`.chomp
+
+ #
+ # Make sure all alerts default to "-r now"
+ #
+ update.alert.each do |alert|
+ next if alert.raise_time || alert.clear_time
+ alert.raise_time = MauveTime.now.to_i
+ end
+
+ #
+ # Make sure we set the transmission time
+ #
+ update.transmission_time = MauveTime.now.to_i
+
+ data = update.serialize_to_string
+
+
+ if verbose == 1
+ print "#{update.transmission_id}\n"
+ elsif verbose >= 2
+ print "Sending #{update.inspect.chomp} to #{@destinations.join(", ")}\n"
+ end
+
+ @destinations.each do |spec|
+ UDPSocket.open do |sock|
+ ip = spec.split(":")[0]
+ port = spec.split(":")[1].to_i
+ sock.send(data, 0, ip, port)
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb
new file mode 100644
index 0000000..30536bb
--- /dev/null
+++ b/lib/mauve/server.rb
@@ -0,0 +1,142 @@
+# encoding: UTF-8
+require 'yaml'
+require 'socket'
+# require 'mauve/datamapper'
+require 'mauve/proto'
+require 'mauve/alert'
+require 'mauve/mauve_thread'
+require 'mauve/mauve_time'
+require 'mauve/timer'
+require 'mauve/udp_server'
+require 'mauve/processor'
+require 'mauve/http_server'
+require 'log4r'
+
+
+module Mauve
+
+ class Server
+
+ DEFAULT_CONFIGURATION = {
+ :ip => "127.0.0.1",
+ :port => 32741,
+ :database => "sqlite3:///./mauvealert.db",
+ :log_file => "stdout",
+ :log_level => 1,
+ :transmission_cache_expire_time => 600
+ }
+
+
+ #
+ # This is the order in which the threads should be started.
+ #
+ THREAD_CLASSES = [UDPServer, HTTPServer, Processor, Notifier, Timer]
+
+ attr_accessor :web_interface
+ attr_reader :stopped_at, :started_at, :initial_sleep
+
+ include Singleton
+
+ def initialize
+ # Set the logger up
+ @logger = Log4r::Logger.new(self.class.to_s)
+
+ # Sleep time between pooling the @buffer buffer.
+ @sleep = 1
+
+ @freeze = false
+ @stop = false
+
+ @stopped_at = MauveTime.now
+ @started_at = MauveTime.now
+ @initial_sleep = 300
+
+ @config = DEFAULT_CONFIGURATION
+ end
+
+ def configure(config_spec = nil)
+ #
+ # Update the configuration
+ #
+ if config_spec.nil?
+ # Do nothing
+ elsif config_spec.kind_of?(String) and File.exists?(config_spec)
+ @config.update(YAML.load_file(config_spec))
+ elsif config_spec.kind_of?(Hash)
+ @config.update(config_spec)
+ else
+ raise ArgumentError.new("Unknown configuration spec "+config_spec.inspect)
+ end
+
+ #
+ DataMapper.setup(:default, @config[:database])
+ DataObjects::Sqlite3.logger = Log4r::Logger.new("Mauve::DataMapper")
+
+ #
+ # Update any tables.
+ #
+ Alert.auto_upgrade!
+ AlertChanged.auto_upgrade!
+ Mauve::AlertEarliestDate.create_view!
+
+ #
+ # Work out when the server was last stopped
+ #
+ @stopped_at = self.last_heartbeat
+ end
+
+ def last_heartbeat
+ #
+ # Work out when the last update was
+ #
+ [ Alert.last(:order => :updated_at.asc),
+ AlertChanged.last(:order => :updated_at.asc) ].
+ reject{|a| a.nil?}.
+ collect{|a| a.updated_at.to_time}.
+ sort.
+ last
+ end
+
+ def freeze
+ @frozen = true
+ end
+
+ def thaw
+ @thaw = true
+ end
+
+ def stop
+ @stop = true
+
+ THREAD_CLASSES.reverse.each do |klass|
+ klass.instance.stop
+ end
+
+ @logger.info("All threads stopped")
+ end
+
+ def run
+ loop do
+ THREAD_CLASSES.each do |klass|
+ next if @frozen or @stop
+
+ unless klass.instance.alive?
+ # ugh something has died.
+ klass.instance.join
+ klass.instance.start unless @stop
+ end
+
+ end
+
+ break if @stop
+
+ sleep 1
+ end
+ logger.debug("Thread stopped")
+ end
+
+ alias start run
+
+ end
+
+end
diff --git a/lib/mauve/source_list.rb b/lib/mauve/source_list.rb
new file mode 100644
index 0000000..4ffef15
--- /dev/null
+++ b/lib/mauve/source_list.rb
@@ -0,0 +1,105 @@
+# encoding: UTF-8
+require 'log4r'
+require 'resolv-replace'
+
+module Mauve
+
+ # A simple construct to match sources.
+ #
+ # This class stores mamed lists of IP addresses. It stores them in a hash
+ # indexed by the name of the list. One can pass IPv4, IPv6 and hostnames
+ # as list elements but they will all be converted into IP addresses at
+ # the time of the list creation.
+ #
+ # One can ask if an IPv4, IPv6, hostname or url (match on hostname only) is
+ # contained within a list. If the query is not an IP address, it will be
+ # converted into one before the checks are made.
+ #
+ # Note that the matching is greedy. When a hostname maps to several IP
+ # addresses and only one of tbhose is included in the list, a match
+ # will occure.
+ #
+ # @author Yann Golanski
+ class SourceList
+
+ # Accessor, read only. Use create_new_list() to create lists.
+ attr_reader :hash
+
+ ## Default contructor.
+ def initialize ()
+ @logger = Log4r::Logger.new "mauve::SourceList"
+ @hash = Hash.new
+ @http_head = Regexp.compile(/^http[s]?:\/\//)
+ @http_tail = Regexp.compile(/\/.*$/)
+ end
+
+ ## Return whether or not a list contains a source.
+ #
+ # @param [String] lst The list name.
+ # @param [String] src The hostname or IP of the source.
+ # @return [Boolean] true if there is such a source, false otherwise.
+ def does_list_contain_source?(lst, src)
+ raise ArgumentError.new("List name must be a String, not a #{lst.class}") if String != lst.class
+ raise ArgumentError.new("Source name must be a String, not a #{src.class}") if String != src.class
+ raise ArgumentError.new("List #{lst} does not exist.") if false == @hash.has_key?(lst)
+ if src.match(@http_head)
+ src = src.gsub(@http_head, '').gsub(@http_tail, '')
+ end
+ begin
+ Resolv.getaddresses(src).each do |ip|
+ return true if @hash[lst].include?(ip)
+ end
+ rescue Resolv::ResolvError, Resolv::ResolvMauveTimeout => e
+ @logger.warn("#{lst} could not be resolved because #{e.message}.")
+ return false
+ rescue => e
+ @logger.error("Unknown exception raised: #{e.class} #{e.message}")
+ return false
+ end
+ return false
+ end
+
+ ## Create a list.
+ #
+ # Note that is no elements give IP addresses, we have an empty list.
+ # This gets logged but otherwise does not stop mauve from working.
+ #
+ # @param [String] name The name of the list.
+ # @param [Array] elem A list of source either hostname or IP.
+ def create_new_list(name, elem)
+ raise ArgumentError.new("Name of list is not a String but a #{name.class}") if String != name.class
+ raise ArgumentError.new("Element list is not an Array but a #{elem.class}") if Array != elem.class
+ raise ArgumentError.new("A list called #{name} already exists.") if @hash.has_key?(name)
+ arr = Array.new
+ elem.each do |host|
+ begin
+ Resolv.getaddresses(host).each do |ip|
+ arr << ip
+ end
+ rescue Resolv::ResolvError, Resolv::ResolvMauveTimeout => e
+ @logger.warn("#{host} could not be resolved because #{e.message}.")
+ rescue => e
+ @logger.error("Unknown exception raised: #{e.class} #{e.message}")
+ end
+ end
+ @hash[name] = arr.flatten.uniq.compact
+ if true == @hash[name].empty?
+ @logger.error("List #{name} is empty! "+
+ "Nothing from element list '#{elem}' "+
+ "has resolved to anything useable.")
+ end
+ end
+
+ end
+
+ ## temporary object to convert from configuration file to the SourceList class
+ class AddSoruceList < Struct.new(:label, :list)
+
+ # Default constructor.
+ def initialize (*args)
+ super(*args)
+ end
+
+ end
+
+end
diff --git a/lib/mauve/timer.rb b/lib/mauve/timer.rb
new file mode 100644
index 0000000..5355dcc
--- /dev/null
+++ b/lib/mauve/timer.rb
@@ -0,0 +1,86 @@
+# encoding: UTF-8
+require 'mauve/alert'
+require 'mauve/notifier'
+require 'mauve/mauve_thread'
+require 'thread'
+require 'log4r'
+
+module Mauve
+
+ class Timer < MauveThread
+
+ include Singleton
+
+ attr_accessor :sleep_interval, :last_run_at
+
+ def initialize
+ @logger = Log4r::Logger.new self.class.to_s
+ @logger.info("Timer singleton created.")
+ @initial_sleep = 300
+ @initial_sleep_threshold = 300
+ end
+
+ def main_loop
+ @logger.debug "hello"
+ #
+ # Get the next alert.
+ #
+ next_alert = Alert.find_next_with_event
+
+ #
+ # If we didn't find an alert, or the alert we found is due in the future,
+ # look for the next alert_changed object.
+ #
+ if next_alert.nil? or next_alert.due_at > MauveTime.now
+ @logger.debug("Next alert was #{next_alert} due at #{next_alert.due_at}") unless next_alert.nil?
+ next_alert_changed = AlertChanged.find_next_with_event
+ end
+
+ if next_alert_changed.nil? and next_alert.nil?
+ next_to_notify = nil
+
+ elsif next_alert.nil? or next_alert_changed.nil?
+ next_to_notify = (next_alert || next_alert_changed)
+
+ else
+ next_to_notify = ( next_alert.due_at < next_alert_changed.due_at ? next_alert : next_alert_changed )
+
+ end
+
+ #
+ # Nothing to notify?
+ #
+ if next_to_notify.nil?
+ #
+ # Sleep indefinitely
+ #
+ @logger.debug("Nothing to notify about -- snoozing indefinitely.")
+ else
+ #
+ # La la la nothing to do.
+ #
+ @logger.debug("Next to notify: #{next_to_notify} -- snoozing until #{next_to_notify.due_at}")
+ end
+
+ #
+ # Ah-ha! Sleep with a break clause.
+ #
+ while next_to_notify.nil? or MauveTime.now <= next_to_notify.due_at
+ #
+ # Start again if the situation has changed.
+ #
+ break if self.should_stop?
+ #
+ # This is a rate-limiting step for alerts.
+ #
+ Kernel.sleep 0.2
+ end
+
+ return if self.should_stop? or next_to_notify.nil?
+
+ next_to_notify.poll
+ end
+
+ end
+
+end
diff --git a/lib/mauve/udp_server.rb b/lib/mauve/udp_server.rb
new file mode 100644
index 0000000..a570e8a
--- /dev/null
+++ b/lib/mauve/udp_server.rb
@@ -0,0 +1,109 @@
+# encoding: UTF-8
+require 'yaml'
+require 'socket'
+require 'mauve/datamapper'
+require 'mauve/proto'
+require 'mauve/alert'
+require 'log4r'
+
+module Mauve
+
+ class UDPServer < MauveThread
+
+ include Singleton
+
+ attr_accessor :ip, :port, :sleep_interval
+
+ def initialize
+ #
+ # Set the logger up
+ #
+ @logger = Log4r::Logger.new(self.class.to_s)
+ @ip = "127.0.0.1"
+ @port = 32741
+ @socket = nil
+ @closing_now = false
+ @sleep_interval = 0
+ end
+
+ def open_socket
+ @socket = UDPSocket.new
+ @closing_now = false
+
+ @logger.debug("Trying to increase Socket::SO_RCVBUF to 10M.")
+ old = @socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF).unpack("i").first
+
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, 10*1024*1024)
+ new = @socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF).unpack("i").first
+
+ raise "Could not increase Socket::SO_RCVBUF. Had #{old} ended up with #{new}!" if old > new
+
+ @logger.debug("Successfully increased Socket::SO_RCVBUF from #{old} to #{new}.")
+
+ @socket.bind(@ip, @port)
+
+ @logger.debug("Successfully opened UDP socket on #{@ip}:#{@port}")
+ end
+
+ def close_socket
+ return if @socket.nil? or @socket.closed?
+
+ begin
+ @socket.close
+ rescue IOError => ex
+ # Just in case there is some sort of explosion!
+ @logger.debug("Caught IOError #{ex.to_s}")
+ end
+
+ @logger.debug("Successfully closed UDP socket")
+ end
+
+ def main_loop
+ return if self.should_stop?
+
+ open_socket if @socket.nil? or @socket.closed?
+
+ return if self.should_stop?
+
+ #
+ # TODO: why is/isn't this non-block?
+ #
+ begin
+ # packet = @socket.recvfrom_nonblock(65535)
+ packet = @socket.recvfrom(65535)
+ received_at = MauveTime.now
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
+ IO.select([@socket])
+ retry unless self.should_stop?
+ end
+
+ return if packet.nil?
+
+ @logger.debug("Got new packet: #{packet.inspect}")
+
+ #
+ # If we get a zero length packet, and we've been flagged to stop, we stop!
+ #
+ if packet.first.length == 0 and self.should_stop?
+ self.close_socket
+ return
+ end
+
+
+
+ Processor.push([[packet[0], packet[1], received_at]])
+ end
+
+ def stop
+ @stop = true
+ #
+ # Triggers loop to close socket.
+ #
+ UDPSocket.open.send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.closed?
+
+ super
+ end
+
+ end
+
+end
diff --git a/lib/mauve/web_interface.rb b/lib/mauve/web_interface.rb
new file mode 100644
index 0000000..4569cb6
--- /dev/null
+++ b/lib/mauve/web_interface.rb
@@ -0,0 +1,334 @@
+# encoding: UTF-8
+require 'sinatra/base'
+require 'sinatra-partials'
+require 'haml'
+require 'rack'
+require 'rack-flash'
+
+if !defined?(JRUBY_VERSION)
+ require 'thin'
+end
+
+module Mauve
+ # Our Sinatra app proper
+ #
+ class WebInterface < Sinatra::Base
+
+ class PleaseAuthenticate < Exception; end
+
+ use Rack::Session::Cookie, :expire_after => 604800 # 7 days in seconds
+
+ enable :sessions
+
+ use Rack::Flash
+
+ set :root, "/usr/share/mauve"
+ set :views, "#{root}/views"
+ set :public, "#{root}/static"
+ set :static, true
+ set :show_exceptions, true
+
+ logger = Log4r::Logger.new("Mauve::WebInterface")
+
+ set :logging, true
+ set :logger, logger
+ set :dump_errors, true # ...will dump errors to the log
+ set :raise_errors, false # ...will not let exceptions out to main program
+ set :show_exceptions, false # ...will not show exceptions
+
+ ########################################################################
+
+ before do
+ @person = Configuration.current.people[session['username']]
+ @title = "Mauve alert panel"
+ end
+
+ get '/' do
+ redirect '/alerts'
+ end
+
+ ########################################################################
+
+ ## Checks the identity of the person via a password.
+ #
+ # The password can be either the SSO or a local one defined
+ # in the configuration file.
+ #
+ post '/login' do
+ usr = params['username']
+ pwd = params['password']
+ ret_sso = helper_auth_SSO(usr, pwd)
+ ret_loc = helper_auth_local(usr, pwd)
+ if "success" == ret_sso or "success" == ret_loc
+ session['username'] = usr
+ else
+ flash['error'] =<<__MSG
+<hr /> <img src="/images/error.png" /> <br />
+ACCESS DENIED <br />
+#{ret_sso} <br />
+#{ret_loc} <hr />
+__MSG
+ end
+ redirect '/alerts'
+ end
+
+ get '/logout' do
+ session.delete('username')
+ redirect '/alerts'
+ end
+
+ get '/alerts' do
+ #now = MauveTime.now.to_f
+ please_authenticate()
+ find_active_alerts()
+ #pp MauveTime.now.to_f - now
+ haml(:alerts2)
+ end
+
+ get '/_alert_summary' do
+ find_active_alerts; partial("alert_summary")
+ end
+
+ get '/_alert_counts' do
+ find_active_alerts; partial("alert_counts")
+ end
+
+ get '/_head' do
+ find_active_alerts()
+ partial("head")
+ end
+
+ get '/alert/:id/detail' do
+ please_authenticate
+
+ content_type("text/html") # I think
+ Alert.get(params[:id]).detail
+ end
+
+ get '/alert/:id' do
+ please_authenticate
+ find_active_alerts
+ @alert = Alert.get(params['id'])
+ haml :alert
+ end
+
+ post '/alert/:id/acknowledge' do
+ please_authenticate
+
+ alert = Alert.get(params[:id])
+ if alert.acknowledged?
+ alert.unacknowledge!
+ else
+ alert.acknowledge!(@person, 0)
+ end
+ content_type("application/json")
+ alert.to_json
+ end
+
+ # Note that :until must be in seconds.
+ post '/alert/acknowledge/:id/:until' do
+ #now = MauveTime.now.to_f
+ please_authenticate
+
+ alert = Alert.get(params[:id])
+ alert.acknowledge!(@person, params[:until].to_i())
+
+ #print "Acknowledge request was processed in #{MauveTime.now.to_f - now} seconds\n"
+ content_type("application/json")
+ alert.to_json
+ end
+
+ post '/alert/:id/raise' do
+ #now = MauveTime.now.to_f
+ please_authenticate
+
+ alert = Alert.get(params[:id])
+ alert.raise!
+ #print "Raise request was processed in #{MauveTime.now.to_f - now} seconds\n"
+ content_type("application/json")
+ alert.to_json
+ end
+
+ post '/alert/:id/clear' do
+ please_authenticate
+
+ alert = Alert.get(params[:id])
+ alert.clear!
+ content_type("application/json")
+ alert.to_json
+ end
+
+ post '/alert/:id/toggleDetailView' do
+ please_authenticate
+
+ alert = Alert.get(params[:id])
+ if nil != alert
+ id = params[:id].to_i()
+ session[:display_alerts][id] = (true == session[:display_alerts][id])? false : true
+ content_type("application/json")
+ 'all is good'.to_json
+ end
+ end
+
+ post '/alert/fold/:subject' do
+ please_authenticate
+
+ session[:display_folding][params[:subject]] = (true == session[:display_folding][params[:subject]])? false : true
+ content_type("application/json")
+ 'all is good'.to_json
+ end
+
+ ########################################################################
+
+ get '/preferences' do
+ please_authenticate
+ find_active_alerts
+ haml :preferences
+ end
+
+ ########################################################################
+
+ get '/events' do
+ please_authenticate
+ find_active_alerts
+ find_recent_alerts
+ haml :events
+ end
+
+ ########################################################################
+
+ helpers do
+ include Sinatra::Partials
+
+ def please_authenticate
+ raise PleaseAuthenticate.new unless @person
+ end
+
+ def find_active_alerts
+
+ # FIXME: make sure alerts only appear once some better way
+ #@urgent = AlertGroup.all_alerts_by_level(:urgent)
+ #@normal = AlertGroup.all_alerts_by_level(:normal) - @urgent
+ #@low = AlertGroup.all_alerts_by_level(:low) - @normal - @urgent
+ ook = Alert.get_all()
+ @urgent = ook[:urgent]
+ @normal = ook[:normal]
+ @low = ook[:low]
+
+ # Get groups of alerts and count those acknowledged.
+ @grouped_ack_urgent = Hash.new()
+ @grouped_ack_normal = Hash.new()
+ @grouped_ack_low = Hash.new()
+ @grouped_new_urgent = Hash.new()
+ @grouped_new_normal = Hash.new()
+ @grouped_new_low = Hash.new()
+ @count_ack = Hash.new()
+ @count_ack[:urgent] = self.group_alerts(@grouped_ack_urgent,
+ @grouped_new_urgent,
+ @urgent)
+ @count_ack[:normal] = self.group_alerts(@grouped_ack_normal,
+ @grouped_new_normal,
+ @normal)
+ @count_ack[:low] = self.group_alerts(@grouped_ack_low,
+ @grouped_new_low,
+ @low)
+ @grouped_ack = Hash.new()
+ @grouped_new = Hash.new()
+ @grouped_ack_urgent.each_pair {|k,v| @grouped_ack[k] = v}
+ @grouped_ack_normal.each_pair {|k,v| @grouped_ack[k] = v}
+ @grouped_ack_low.each_pair {|k,v| @grouped_ack[k] = v}
+ @grouped_new_urgent.each_pair {|k,v| @grouped_new[k] = v}
+ @grouped_new_normal.each_pair {|k,v| @grouped_new[k] = v}
+ @grouped_new_low.each_pair {|k,v| @grouped_new[k] = v}
+ end
+
+ ## Fill two hashs with alerts that are acknowledged or not.
+ # @param [Hash] ack Acknowledge hash.
+ # @param [Hash] new Unacknowledged (aka new) hash.
+ # @param [List] list List of alerts.
+ # @return [Fixnum] The count of acknowledged alerts.
+ def group_alerts(ack, new, list)
+ count = 0
+ list.each do |alert|
+ #key = alert.source + '::' + alert.subject
+ key = alert.subject
+ if true == alert.acknowledged?
+ count += 1
+ ack[key] = Array.new() if false == ack.has_key?(key)
+ ack[key] << alert
+ else
+ new[key] = Array.new() if false == new.has_key?(key)
+ new[key] << alert
+ end
+ if false == session[:display_alerts].has_key?(alert.id)
+ session[:display_alerts][alert.id] = false
+ end
+ if false == session[:display_folding].has_key?(key)
+ session[:display_folding][key] = false
+ end
+ #session[:display_alerts][alert.id] = true if false == session[:display_alerts].has_key?(alert.id)
+ #session[:display_folding][key] = true if false == session[:display_folding].has_key?(key)
+ new.each_key {|k| new[k].sort!{|a,b| a.summary <=> b.summary} }
+ ack.each_key {|k| ack[k].sort!{|a,b| a.summary <=> b.summary} }
+ end
+ return count
+ end
+
+ def find_recent_alerts
+ since = params['since'] ? MauveTime.parse(params['since']) : (MauveTime.now-86400)
+ @alerts = Alert.all(:updated_at.gt => since, :order => [:raised_at.desc, :cleared_at.desc, :acknowledged_at.desc, :updated_at.desc, ])
+ end
+
+ def cycle(*list)
+ @cycle ||= 0
+ @cycle = (@cycle + 1) % list.length
+ list[@cycle]
+ end
+
+ ## Test for authentication with SSO.
+ #
+ def helper_auth_SSO (usr, pwd)
+ auth = AuthSourceBytemark.new()
+ begin
+ return "success" if true == auth.authenticate(usr,pwd)
+ return "SSO did not regcognise your login/password combination."
+ rescue ArgumentError => ex
+ return "SSO argument error: #{ex.message}"
+ rescue => ex
+ return "SSO generic error: #{ex.message}"
+ end
+ end
+
+ ## Test for authentication with configuration file parameter.
+ #
+ def helper_auth_local (usr, pwd)
+ person = Configuration.current.people[params['username']]
+ return "I did not recognise your local login details." if !person
+ return "I did not recognise your local password." if Digest::SHA1.hexdigest(params['password']) != person.password
+ return "success"
+ end
+
+ end
+
+ ########################################################################
+
+ error PleaseAuthenticate do
+ status 403
+ session[:display_alerts] = Hash.new()
+ session[:display_folding] = Hash.new()
+ haml :please_authenticate
+ end
+
+ ########################################################################
+ # @see http://stackoverflow.com/questions/2239240/use-rackcommonlogger-in-sinatra
+ def call(env)
+ if true == @logger.nil?
+ @logger = Log4r::Logger.new("mauve::Rack")
+ end
+ env['rack.errors'] = RackErrorsProxy.new(@logger)
+ super(env)
+ end
+
+ end
+
+end
diff --git a/lib/object_builder.rb b/lib/object_builder.rb
new file mode 100644
index 0000000..7cb808c
--- /dev/null
+++ b/lib/object_builder.rb
@@ -0,0 +1,113 @@
+# ObjectBuilder is a class to help you build Ruby-based configuration syntaxes.
+# You can use it to make "builder" classes to help build particular types
+# of objects, typically translating simple command-based syntax to creating
+# classes and setting attributes. e.g. here is a description of a day at
+# the zoo:
+#
+# person "Alice"
+# person "Matthew"
+#
+# zoo("London") {
+# enclosure("Butterfly House") {
+#
+# has_roof
+# allow_visitors
+#
+# animals("moth", 10) {
+# wings 2
+# legs 2
+# }
+#
+# animals("butterfly", 200) {
+# wings 2
+# legs 2
+# }
+# }
+#
+# enclosure("Aquarium") {
+# no_roof
+#
+# animal("killer whale") {
+# called "Shamu"
+# wings 0
+# legs 0
+# tail
+# }
+# }
+# }
+#
+# Here is the basic builder class for a Zoo...
+#
+# TODO: finish this convoluted example, if it kills me
+#
+class ObjectBuilder
+ class BuildException < Exception; end
+
+ attr_reader :result
+
+ def initialize(context, *args)
+ @context = context
+ builder_setup(*args)
+ end
+
+ def anonymous_name
+ @@sequence ||= 0 # not inherited, don't want it to be
+ @@sequence += 1
+ "anon.#{Time.now.to_i}.#{@@sequence}"
+ end
+
+ class << self
+
+ def is_builder(word, clazz)
+ define_method(word.to_sym) do |*args, &block|
+ builder = clazz.new(*([@context] + args))
+ builder.instance_eval(&block) if block
+ ["created_#{word}", "created"].each do |created_method|
+ created_method = created_method.to_sym
+ if respond_to?(created_method)
+ __send__(created_method, builder.result)
+ break
+ end
+ end
+ end
+ end
+
+ # FIXME: implement is_builder_deferred to create object at end of block?
+
+ def is_block_attribute(word)
+ define_method(word.to_sym) do |*args, &block|
+ @result.__send__("#{word}=".to_sym, block)
+ end
+ end
+
+ def is_attribute(word)
+ define_method(word.to_sym) do |*args, &block|
+ @result.__send__("#{word}=".to_sym, args[0])
+ end
+ end
+
+ def is_flag_attribute(word)
+ define_method(word.to_sym) do |*args, &block|
+ @result.__send__("#{word}=".to_sym, true)
+ end
+ end
+
+ def load(file)
+ builder = self.new
+ builder.instance_eval(File.read(file), file)
+ builder.result
+ end
+
+ def inherited(*args)
+ initialize_class
+ end
+
+ def initialize_class
+ @words = {}
+ end
+ end
+
+ initialize_class
+end
+
+
diff --git a/lib/rack-flash.rb b/lib/rack-flash.rb
new file mode 100644
index 0000000..bc95b21
--- /dev/null
+++ b/lib/rack-flash.rb
@@ -0,0 +1,171 @@
+#
+# borrowed from http://github.com/nakajima/rack-flash - thanks!
+#
+
+require 'rack/builder'
+
+module Rack
+ class Builder
+ attr :ins
+ def use(middleware, *args, &block)
+ middleware.instance_variable_set "@rack_builder", self
+ def middleware.rack_builder
+ @rack_builder
+ end
+ @ins << lambda { |app|
+ middleware.new(app, *args, &block)
+ }
+ end
+
+ def run(app)
+ klass = app.class
+ klass.instance_variable_set "@rack_builder", self
+ def klass.rack_builder
+ @rack_builder
+ end
+ @ins << app #lambda { |nothing| app }
+ end
+
+ def leaf_app
+ ins.last
+ end
+ end
+end
+
+module Rack
+ class Flash
+ # Raised when the session passed to FlashHash initialize is nil. This
+ # is usually an indicator that session middleware is not in use.
+ class SessionUnavailable < StandardError; end
+
+ # Implements bracket accessors for storing and retrieving flash entries.
+ class FlashHash
+ attr_reader :flagged
+
+ def initialize(store, opts={})
+ raise Rack::Flash::SessionUnavailable \
+ .new('Rack::Flash depends on session middleware.') unless store
+
+ @opts = opts
+ @store = store
+
+ if accessors = @opts[:accessorize]
+ accessors.each { |opt| def_accessor(opt) }
+ end
+ end
+
+ # Remove an entry from the session and return its value. Cache result in
+ # the instance cache.
+ def [](key)
+ key = key.to_sym
+ cache[key] ||= values.delete(key)
+ end
+
+ # Store the entry in the session, updating the instance cache as well.
+ def []=(key,val)
+ key = key.to_sym
+ cache[key] = values[key] = val
+ end
+
+ # Store a flash entry for only the current request, swept regardless of
+ # whether or not it was actually accessed. Useful for AJAX requests, where
+ # you want a flash message, even though you're response isn't redirecting.
+ def now
+ cache
+ end
+
+ # Checks for the presence of a flash entry without retrieving or removing
+ # it from the cache or store.
+ def has?(key)
+ [cache, values].any? { |store| store.keys.include?(key.to_sym) }
+ end
+ alias_method :include?, :has?
+
+ # Mark existing entries to allow for sweeping.
+ def flag!
+ @flagged = values.keys
+ end
+
+ # Remove flagged entries from flash session, clear flagged list.
+ def sweep!
+ Array(flagged).each { |key| values.delete(key) }
+ flagged.clear
+ end
+
+ # Hide the underlying :__FLASH__ session key and only expose values stored
+ # in the flash.
+ def inspect
+ '#<FlashHash @values=%s @cache=%s>' % [values.inspect, cache.inspect]
+ end
+
+ # Human readable for logging.
+ def to_s
+ values.inspect
+ end
+
+ private
+
+ # Maintain an instance-level cache of retrieved flash entries. These
+ # entries will have been removed from the session, but are still available
+ # through the cache.
+ def cache
+ @cache ||= {}
+ end
+
+ # Helper to access flash entries from :__FLASH__ session value. This key
+ # is used to prevent collisions with other user-defined session values.
+ def values
+ @store[:__FLASH__] ||= {}
+ end
+
+ # Generate accessor methods for the given entry key if :accessorize is true.
+ def def_accessor(key)
+ raise ArgumentError.new('Invalid entry type: %s' % key) if respond_to?(key)
+
+ class << self; self end.class_eval do
+ define_method(key) { |*args| val = args.first; val ? (self[key]=val) : self[key] }
+ define_method("#{key}=") { |val| self[key] = val }
+ define_method("#{key}!") { |val| cache[key] = val }
+ end
+ end
+ end
+
+ # -------------------------------------------------------------------------
+ # - Rack Middleware implementation
+
+ def initialize(app, opts={})
+ if klass = app_class(app, opts)
+ klass.class_eval do
+ def flash; env['x-rack.flash'] end
+ end
+ end
+
+ @app, @opts = app, opts
+ end
+
+ def call(env)
+ env['x-rack.flash'] ||= Rack::Flash::FlashHash.new(env['rack.session'], @opts)
+
+ if @opts[:sweep]
+ env['x-rack.flash'].flag!
+ end
+
+ res = @app.call(env)
+
+ if @opts[:sweep]
+ env['x-rack.flash'].sweep!
+ end
+
+ res
+ end
+
+ private
+
+ def app_class(app, opts)
+ return nil if opts.has_key?(:helper) and not opts[:helper]
+ opts[:flash_app_class] ||
+ defined?(Sinatra::Base) && Sinatra::Base ||
+ self.class.rack_builder.leaf_app.class
+ end
+ end
+end
diff --git a/lib/sinatra-partials.rb b/lib/sinatra-partials.rb
new file mode 100644
index 0000000..e16313c
--- /dev/null
+++ b/lib/sinatra-partials.rb
@@ -0,0 +1,25 @@
+#
+# stolen from http://github.com/cschneid/irclogger/blob/master/lib/partials.rb
+# and made a lot more robust by me
+#
+# this implementation uses erb by default. if you want to use any other template mechanism
+# then replace `erb` on line 13 and line 17 with `haml` or whatever
+#
+
+module Sinatra::Partials
+ def partial(template, *args)
+ template_array = template.to_s.split('/')
+ template = template_array[0..-2].join('/') + "/_#{template_array[-1]}"
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ options.merge!(:layout => false)
+ if collection = options.delete(:collection) then
+ collection.inject([]) do |buffer, member|
+ buffer << erb(:"#{template}", options.merge(:layout =>
+ false, :locals => {template_array[-1].to_sym => member}))
+ end.join("\n")
+ else
+ haml(:"#{template}", options)
+ end
+ end
+end
+