aboutsummaryrefslogtreecommitdiff
path: root/lib/mauve
diff options
context:
space:
mode:
Diffstat (limited to 'lib/mauve')
-rw-r--r--lib/mauve/alert.rb189
-rw-r--r--lib/mauve/alert_changed.rb51
-rw-r--r--lib/mauve/alert_group.rb41
-rw-r--r--lib/mauve/authentication.rb65
-rw-r--r--lib/mauve/calendar_interface.rb23
-rw-r--r--lib/mauve/configuration.rb38
-rw-r--r--lib/mauve/configuration_builder.rb18
-rw-r--r--lib/mauve/configuration_builders.rb11
-rw-r--r--lib/mauve/configuration_builders/alert_group.rb101
-rw-r--r--lib/mauve/configuration_builders/logger.rb64
-rw-r--r--lib/mauve/configuration_builders/notification_method.rb34
-rw-r--r--lib/mauve/configuration_builders/person.rb41
-rw-r--r--lib/mauve/configuration_builders/server.rb102
-rw-r--r--lib/mauve/heartbeat.rb30
-rw-r--r--lib/mauve/history.rb31
-rw-r--r--lib/mauve/http_server.rb88
-rw-r--r--lib/mauve/mauve_resolv.rb16
-rw-r--r--lib/mauve/mauve_thread.rb189
-rw-r--r--lib/mauve/mauve_time.rb75
-rw-r--r--lib/mauve/notification.rb88
-rw-r--r--lib/mauve/notifier.rb81
-rw-r--r--lib/mauve/notifiers.rb9
-rw-r--r--lib/mauve/notifiers/email.rb6
-rw-r--r--lib/mauve/notifiers/xmpp.rb57
-rw-r--r--lib/mauve/people_list.rb19
-rw-r--r--lib/mauve/person.rb88
-rw-r--r--lib/mauve/pop3_server.rb164
-rw-r--r--lib/mauve/processor.rb84
-rw-r--r--lib/mauve/sender.rb37
-rw-r--r--lib/mauve/server.rb104
-rw-r--r--lib/mauve/source_list.rb64
-rw-r--r--lib/mauve/timer.rb11
-rw-r--r--lib/mauve/udp_server.rb67
-rw-r--r--lib/mauve/version.rb5
34 files changed, 1598 insertions, 493 deletions
diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb
index fa2ce65..25cee54 100644
--- a/lib/mauve/alert.rb
+++ b/lib/mauve/alert.rb
@@ -6,6 +6,10 @@ require 'mauve/source_list'
require 'sanitize'
module Mauve
+ #
+ # This is a view of the Alert table, which allows easy finding of the next
+ # alert due to trigger.
+ #
class AlertEarliestDate
include DataMapper::Resource
@@ -52,9 +56,14 @@ module Mauve
end
end
-
+
+ #
+ # Woo! An alert.
+ #
class Alert
+ # @deprecated Not used anymore?
def bytesize; 99; end
+ # @deprecated Not used anymore?
def size; 99; end
include DataMapper::Resource
@@ -89,31 +98,23 @@ module Mauve
validates_with_method :check_dates
+ default_scope(:default).update(:order => [:source, :importance])
+
+ # @return [String]
def to_s
"#<Alert #{id}, alert_id #{alert_id}, source #{source}>"
end
- #
- # This is to stop datamapper inserting duff dates into the database.
- #
- def check_dates
- bad_dates = self.attributes.find_all do |key, value|
- value.is_a?(Time) and (value < (Time.now - 3650.days) or value > (Time.now + 3650.days))
- 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])
-
+ # @return [Log4r::Logger] the logger instance.
def logger
@logger ||= self.class.logger
end
+ # @deprecated Not sure if this is used any more.
+ #
+ # @param [Integer] Seconds
+ #
+ # @return [String]
def time_relative(secs)
secs = secs.to_i.abs
case secs
@@ -127,45 +128,63 @@ module Mauve
end
#
- # AlertGroup.matches must always return a an array of groups.
- #
+ # @return [Mauve::AlertGroup] The first matching AlertGroup for this alert
def alert_group
@alert_group ||= AlertGroup.matches(self).first
end
- #
# Pick out the source lists that match this alert by subject.
#
+ # @return [Array] All the SourceList matches
def source_lists
Mauve::Configuration.current.source_lists.select{|label, list| list.includes?(self.subject)}.collect{|sl| sl.first}
end
- def in_source_list?(g)
- list = Mauve::Configuration.current.source_lists[g]
+ # Checks to see if included in a named source list
+ #
+ # @param [String] listname
+ # @return [Boolean]
+ def in_source_list?(listname)
+ list = Mauve::Configuration.current.source_lists[listname]
+ return false unless list.is_a?(SourceList)
list.includes?(self.subject)
end
+ # Returns the alert level
#
- #
- #
+ # @return [Symbol] The alert level, as per its AlertGroup.
def level
@level ||= self.alert_group.level
end
+ # An array used to sort compare
+ #
+ # @return [Array]
def sort_tuple
[AlertGroup::LEVELS.index(self.level), (self.raised_at || self.cleared_at || Time.now)]
end
+ # Comparator. Uses sort_tuple to compare with another alert
+ #
+ # @param [Mauve::Alert] other Other alert
+ #
+ # @return [Integer]
def <=>(other)
other.sort_tuple <=> self.sort_tuple
end
+ # The alert subject
+ #
+ # @return [String]
def subject; attribute_get(:subject) || attribute_get(:source) || "not set" ; end
+
+ # The alert detail
+ #
+ # @return [String]
def detail; attribute_get(:detail) || "_No detail set._" ; end
protected
- #
# This cleans the HTML before saving.
#
def do_sanitize_html
@@ -185,8 +204,21 @@ module Mauve
attribute_set(key, Alert.clean_html(val))
end
end
-
+
+ # This is to stop datamapper inserting duff dates into the database.
#
+ def check_dates
+ bad_dates = self.attributes.find_all do |key, value|
+ value.is_a?(Time) and (value < (Time.now - 3650.days) or value > (Time.now + 3650.days))
+ end
+
+ if bad_dates.empty?
+ true
+ else
+ [ false, "The dates "+bad_dates.collect{|k,v| "#{v.to_s} (#{k})"}.join(", ")+" are invalid." ]
+ end
+ end
+
# This allows us to take a copy of the changes before we save.
#
def take_copy_of_changes
@@ -196,9 +228,9 @@ module Mauve
end
end
- #
# This sends notifications. It is called after each save.
#
+ # @return [Boolean]
def notify_if_needed
#
# Make sure we don't barf
@@ -243,20 +275,33 @@ module Mauve
true
end
+ # Remove all history for an alert, when an alert is destroyed.
+ #
+ #
def destroy_associations
AlertHistory.all(:alert_id => self.id).destroy
end
public
+ # Send a notification for this alert.
+ #
+ # @return [Boolean] Showing if an alert has been sent.
def notify
if self.alert_group.nil?
logger.warn "Could not notify for #{self} since there are no matching alert groups"
+ false
else
self.alert_group.notify(self)
end
end
+ # Acknowledge an alert
+ #
+ # @param [Mauve::Person] person The person acknowledging the alert
+ # @param [Time] ack_until The time when the alert should unacknowledge
+ #
+ # @return [Boolean] showing the acknowledgment has been successful
def acknowledge!(person, ack_until = Time.now+3600)
raise ArgumentError unless person.is_a?(Person)
raise ArgumentError unless ack_until.is_a?(Time)
@@ -281,6 +326,9 @@ module Mauve
end
end
+ # Unacknowledge an alert
+ #
+ # @return [Boolean] showing the unacknowledgment has been successful
def unacknowledge!
self.acknowledged_by = nil
self.acknowledged_at = nil
@@ -294,7 +342,12 @@ module Mauve
true
end
end
-
+
+ # Raise an alert at a specified time
+ #
+ # @param [Time] at The time at which the alert should be raised.
+ #
+ # @return [Boolean] showing the raise has been successful
def raise!(at = Time.now)
#
# OK if this is an alert updated in the last run, do not raise, just postpone.
@@ -334,6 +387,11 @@ module Mauve
end
end
+ # Clear an alert at a specified time
+ #
+ # @param [Time] at The time at which the alert should be cleared.
+ #
+ # @return [Boolean] showing the clear has been successful
def clear!(at = Time.now)
#
# Postpone clearance if we're in the sleep period.
@@ -379,47 +437,57 @@ module Mauve
end
end
- # Returns the time at which a timer loop should call poll_event to either
- # raise, clear or unacknowldge this event.
- #
+ # The next time this alert should be polled, either to raise, clear, or
+ # unacknowledge, or nil if nothing is due.
+ #
+ # @return [Time, NilClass]
def due_at
[will_clear_at, will_raise_at, will_unacknowledge_at].compact.sort.first
end
+ # Polls the alert, raising or clearing as needed.
+ #
+ # @return [Boolean] showing the poll was successful
def poll
logger.debug("Polling #{self.to_s}")
- raise! if (will_unacknowledge_at and will_unacknowledge_at <= Time.now) or
+ if (will_unacknowledge_at and will_unacknowledge_at <= Time.now) or
(will_raise_at and will_raise_at <= Time.now)
- clear! if will_clear_at && will_clear_at <= Time.now
+ raise!
+ elsif will_clear_at && will_clear_at <= Time.now
+ clear!
+ else
+ true
+ end
end
+ # Is the alert raised?
#
- # Tests to see if an alert is raised/acknowledged given a certain set of
- # dates/times.
- #
- #
-
+ # @return [Boolean]
def raised?
!raised_at.nil? and (cleared_at.nil? or raised_at > cleared_at)
end
+ # Is the alert acknowledged?
+ #
+ # @return [Boolean]
def acknowledged?
!acknowledged_at.nil?
end
+ # Is the alert cleared? Cleared is just the opposite of raised.
#
- # Cleared is just the opposite of raised.
- #
+ # @return [Boolean]
def cleared?
!raised?
end
class << self
+ # Removes HTML from a string
#
- # Utility methods to clean/remove html
- #
+ # @param [String] txt String to clean
+ # @return [String]
def remove_html(txt)
Sanitize.clean(
txt.to_s,
@@ -427,6 +495,10 @@ module Mauve
)
end
+ # Cleans HTML in a string, removing dangerous elements/contents.
+ #
+ # @param [String] txt String to clean
+ # @return [String]
def clean_html(txt)
Sanitize.clean(
txt.to_s,
@@ -434,22 +506,30 @@ module Mauve
)
end
+ # All alerts currently raised
#
- # Find stuff
- #
- #
+ # @return [Array]
def all_raised
all(:raised_at.not => nil, :order => [:raised_at.asc]) & (all(:cleared_at => nil) | all(:raised_at.gte => :cleared_at))
end
+ # All alerts currently raised and unacknowledged
+ #
+ # @return [Array]
def all_unacknowledged
all_raised - all_acknowledged
end
+ # All alerts currently acknowledged
+ #
+ # @return [Array]
def all_acknowledged
all(:acknowledged_at.not => nil)
end
+ # All alerts currently cleared
+ #
+ # @return [Array]
def all_cleared
all - all_raised - all_acknowledged
end
@@ -469,24 +549,31 @@ module Mauve
return hash
end
- #
- # Returns the next Alert that will have a timed action due on it, or nil
- # if none are pending.
+ # Find the next Alert that will have a timed action due on it, or nil if
+ # none are pending.
#
+ # @return [Mauve::Alert, Nilclass]
def find_next_with_event
earliest_alert = AlertEarliestDate.first(:order => [:earliest])
earliest_alert ? earliest_alert.alert : nil
end
+ # @deprecated Not sure this is used any more.
+ #
+ # @return [Array]
def all_overdue(at = Time.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.
#
+ # @param [String] update The update string, as received over UDP
+ # @param [Time] reception_time The time the update was received
+ # @param [String] ip_source The IP address of the source of the update
+ #
+ # @return [NilClass]
def receive_update(update, reception_time = Time.now, ip_source="network")
update = Proto::AlertUpdate.parse_from_string(update) unless update.kind_of?(Proto::AlertUpdate)
@@ -631,6 +718,8 @@ module Mauve
return nil
end
+ #
+ # @return [Log4r::Logger] The class logger
def logger
Log4r::Logger.new(self.to_s)
end
diff --git a/lib/mauve/alert_changed.rb b/lib/mauve/alert_changed.rb
index ac864a0..c36c4c8 100644
--- a/lib/mauve/alert_changed.rb
+++ b/lib/mauve/alert_changed.rb
@@ -3,6 +3,9 @@ require 'mauve/datamapper'
require 'log4r'
module Mauve
+ #
+ # Class to record changes to alerts. Also responsible for keeping records for reminders.
+ #
class AlertChanged
include DataMapper::Resource
@@ -17,26 +20,37 @@ module Mauve
property :level, String, :required => true
property :update_type, String, :required => true
property :remind_at, Time
- # property :updated_at, Time, :required => true
+
+ belongs_to :alert
+
+ # @return [String]
def to_s
"#<AlertChanged #{id}: alert_id #{alert_id}, for #{person}, update_type #{update_type}>"
end
- belongs_to :alert
-
+ # @deprecated I don't think was_relevant is used any more.
+ #
def was_relevant=(value)
attribute_set(:was_relevant, value)
end
+ # The time this object was last updated
+ #
+ # @return [Time]
def updated_at
self.at
end
+ # Set the time this object was last updated
+ #
+ # @param [Time] t
+ # @return [Time]
def updated_at=(t)
self.at = t
end
+ # @return [Log4r::Logger]
def logger
Log4r::Logger.new self.class.to_s
end
@@ -44,6 +58,7 @@ module Mauve
# Sends a reminder about this alert state change, or forget about it if
# the alert has been acknowledged
#
+ # @return [Boolean] indicating successful update of the AlertChanged object
def remind
unless alert.is_a?(Alert)
logger.info "#{self.inspect} lost alert #{alert_id}. Killing self."
@@ -83,28 +98,52 @@ module Mauve
save
end
- def due_at # mimic interface from Alert
+ # The time this AlertChanged should next be polled at, or nil. Mimics
+ # interaface from Alert.
+ #
+ # @return [Time, NilClass]
+ def due_at
remind_at ? remind_at : nil
end
+ # Sends a reminder, if needed. Mimics interaface from Alert.
+ #
+ # @return [Boolean] showing polling was successful
def poll # mimic interface from Alert
logger.debug("Polling #{self.to_s}")
- remind if remind_at.is_a?(Time) and remind_at <= Time.now
+
+ if remind_at.is_a?(Time) and remind_at <= Time.now
+ remind
+ else
+ true
+ end
end
+ # The AlertGroup for this object
+ #
+ # @return [Mauve::AlertGroup]
def alert_group
alert.alert_group
end
class << self
+ # Finds the next reminder due, or nil if nothing due.
+ #
+ # @return [Mauve::AlertChanged, NilClass]
def next_reminder
first(:remind_at.not => nil, :order => [:remind_at])
end
- def find_next_with_event # mimic interface from Alert
+ # Finds the next event due. Mimics interface from Alert.
+ #
+ # @return [Mauve::AlertChanged, NilClass]
+ def find_next_with_event
next_reminder
end
+ # @deprecated I don't think this is used any more.
+ #
+ # @return [Array]
def all_overdue(at = Time.now)
all(:remind_at.not => nil, :remind_at.lt => at, :order => [:remind_at]).to_a
end
diff --git a/lib/mauve/alert_group.rb b/lib/mauve/alert_group.rb
index b320f18..7ea7ffb 100644
--- a/lib/mauve/alert_group.rb
+++ b/lib/mauve/alert_group.rb
@@ -3,6 +3,11 @@ require 'mauve/alert'
require 'log4r'
module Mauve
+ #
+ # This corresponds to a alert_group clause in the configuration. It is what
+ # is used to classify alerts into levels, and thus who gets notified about it
+ # and when.
+ #
class AlertGroup < Struct.new(:name, :includes, :acknowledgement_time, :level, :notifications)
#
@@ -15,6 +20,11 @@ module Mauve
class << self
+ # Finds all AlertGroups that match an alert.
+ #
+ # @param [Mauve::Alert] alert
+ #
+ # @return [Array] AlertGroups that match
def matches(alert)
grps = all.select { |alert_group| alert_group.includes?(alert) }
@@ -26,10 +36,14 @@ module Mauve
grps
end
+ # @return [Log4r::Logger]
def logger
Log4r::Logger.new self.to_s
end
+ # All AlertGroups
+ #
+ # @return [Array]
def all
return [] if Configuration.current.nil?
@@ -44,6 +58,7 @@ module Mauve
# 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().
#
+ # @return [Array]
def all_alerts_by_level(level)
Configuration.current.alert_groups.map do |alert_group|
alert_group.level == level ? alert_group.current_alerts : []
@@ -52,18 +67,26 @@ module Mauve
end
+ # Creates a new AlertGroup
+ #
+ # @param name Name of alert group
+ #
+ # @return [AlertGroup] self
def initialize(name)
self.name = name
self.level = :normal
self.includes = Proc.new { true }
+ self
end
- def inspect
+ # @return [String]
+ def to_s
"#<AlertGroup:#{name} (level #{level})>"
end
# The list of current raised alerts in this group.
#
+ # @return [Array] Array of Mauve::Alert
def current_alerts
Alert.all(:cleared_at => nil, :raised_at.not => nil).select { |a| includes?(a) }
end
@@ -71,7 +94,8 @@ module Mauve
# 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.
+ # @param [Mauve::Alert] alert
+ #
# @return [Boolean] Success or failure.
def includes?(alert)
@@ -86,18 +110,22 @@ module Mauve
alias matches_alert? includes?
+ # @return [Log4r::Logger]
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.
- #
+ #
+ # @param [Mauve::Alert] alert
+ #
+ # @return [Boolean] indicates success or failure of alert.
def notify(alert)
#
# If there are no notifications defined.
#
if notifications.nil?
logger.warn("No notifications found for #{self.inspect}")
- return
+ return false
end
#
@@ -136,12 +164,15 @@ module Mauve
sent_to << notification.notify(alert, sent_to)
end
+ return (sent_to.length > 0)
end
- #
# This sorts by priority (urgent first), and then alphabetically, so the
# first match is the most urgent.
#
+ # @param [Mauve::AlertGroup] other
+ #
+ # @return [Integer]
def <=>(other)
[LEVELS.index(self.level), self.name] <=> [LEVELS.index(other.level), other.name]
end
diff --git a/lib/mauve/authentication.rb b/lib/mauve/authentication.rb
index 74c6780..d0d4596 100644
--- a/lib/mauve/authentication.rb
+++ b/lib/mauve/authentication.rb
@@ -5,10 +5,19 @@ require 'timeout'
module Mauve
+ #
+ # Base class for authentication.
+ #
class Authentication
ORDER = []
+ # Autenticates a user.
+ #
+ # @param [String] login
+ # @param [String] password
+ #
+ # @return [FalseClass] Always returns false.
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
@@ -19,18 +28,28 @@ module Mauve
false
end
+ # @return [Log4r::Logger]
def logger
self.class.logger
end
-
+
+ # @return [Log4r::Logger]
def self.logger
@logger ||= Log4r::Logger.new(self.to_s)
end
+ # This calls all classes in the ORDER array one by one. If all classes
+ # fail, a 5 second sleep rate-limits authentication attempts.
+ #
+ # @param [String] login
+ # @param [String] password
+ #
+ # @return [Boolean] Success or failure.
+ #
def self.authenticate(login, password)
result = false
- ORDER.each do |klass|
+ ORDER.any? do |klass|
auth = klass.new
result = begin
@@ -41,10 +60,9 @@ module Mauve
false
end
- if true == result
- logger.info "Authenticated #{login} using #{auth.class.to_s}"
- break
- end
+ logger.info "Authenticated #{login} using #{auth.class.to_s}" if true == result
+
+ result
end
unless true == result
@@ -59,12 +77,20 @@ module Mauve
end
+ # This is the Bytemark authentication mechansim.
+ #
class AuthBytemark < Authentication
Mauve::Authentication::ORDER << self
+ # Set up the Bytemark authenticator
#
- # TODO: allow configuration of where the server is.
+ # @todo allow configuration of where the server is.
+ #
+ # @param [String] srv Authentication server name
+ # @param [String] port Port overwhich authentication should take place
+ #
+ # @return [Mauve::AuthBytemark]
#
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
@@ -72,10 +98,16 @@ module Mauve
@srv = srv
@port = port
@timeout = 7
+
+ self
end
- ## Not really needed.
- def ping ()
+ # Tests to see if a server is alive, alive-o.
+ #
+ # @deprecated Not really needed.
+ #
+ # @return [Boolean]
+ def ping
begin
Timeout.timeout(@timeout) do
s = TCPSocket.open(@srv, @port)
@@ -90,6 +122,12 @@ module Mauve
return false
end
+ # Authenticate against the Bytemark server
+ #
+ # @param [String] login
+ # @param [String] password
+ #
+ # @return [Boolean]
def authenticate(login, password)
super
@@ -111,10 +149,19 @@ module Mauve
end
+ # This is the local authentication mechansim, i.e. against the values in the
+ # Mauve config file.
+ #
class AuthLocal < Authentication
Mauve::Authentication::ORDER << self
+ # Authenticate against the local configuration
+ #
+ # @param [String] login
+ # @param [String] password
+ #
+ # @return [Boolean]
def authenticate(login,password)
super
Digest::SHA1.hexdigest(password) == Mauve::Configuration.current.people[login].password
diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb
index abce824..cf515d1 100644
--- a/lib/mauve/calendar_interface.rb
+++ b/lib/mauve/calendar_interface.rb
@@ -8,25 +8,20 @@ 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
class << self
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.to_s)
end
# 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.
+ #
+ # @return [Array] A list of all the usernames on support.
def get_users_on_support(url)
result = do_get_with_cache(url)
@@ -41,10 +36,9 @@ module Mauve
# 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 is_user_on_support?(url, usr)
return get_users_on_support(url).include?(usr)
@@ -56,6 +50,7 @@ module Mauve
#
# @param [String] url A Calendar API url.
# @param [String] usr User single sign on.
+ #
# @return [Boolean] True if on holiday, false otherwise.
def is_user_on_holiday?(url)
result = do_get_with_cache(url)
@@ -72,7 +67,7 @@ module Mauve
# Grab a URL from the wide web.
#
- # @TODO: boot this in its own class since list of ips will need it too.
+ # @todo boot this in its own class since list of ips will need it too.
#
# @param [String] uri -- a URL
# @return [String or nil] -- the contents of the URI or nil if an error has been encountered.
@@ -136,6 +131,12 @@ module Mauve
return nil
end
+ # This does HTTP fetches with a 5 minute cache
+ #
+ # @param [String] url
+ # @param [Time] cache_until
+ #
+ # @return [String or nil]
def do_get_with_cache(url, cache_until = Time.now + 5.minutes)
@cache ||= {}
diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb
index 56f5e7e..a74165b 100644
--- a/lib/mauve/configuration.rb
+++ b/lib/mauve/configuration.rb
@@ -4,26 +4,52 @@ require 'mauve/mauve_time'
module Mauve
- ## Configuration object for Mauve.
- #
- #
- # @TODO Write some more documentation. This is woefully inadequate.
+ # Configuration object for Mauve. This is used as the context in
+ # Mauve::ConfigurationBuilder.
#
class Configuration
class << self
+ # The current configuration
+ # @param [Mauve::Configuration]
+ # @return [Mauve::Configuration]
attr_accessor :current
end
-
+
+ # The Server instance
+ # @return [Mauve::Server]
attr_accessor :server
+
+ # The last AlertGroup to be configured
+ # @return [Mauve::AlertGroup]
attr_accessor :last_alert_group
+
+ # Notification methods
+ # @return [Hash]
attr_reader :notification_methods
+
+ # People
+ # @return [Hash]
attr_reader :people
+
+ # Alert groups
+ # @return [Array]
attr_reader :alert_groups
+
+ # People lists
+ # @return [Hash]
attr_reader :people_lists
+
+ # The source lists
+ # @return [Hash]
attr_reader :source_lists
-
+
+ #
+ # Set up a base config.
+ #
def initialize
+ @server = nil
+ @last_alert_group = nil
@notification_methods = {}
@people = {}
@people_lists = Hash.new{|h,k| h[k] = Mauve::PeopleList.new(k)}
diff --git a/lib/mauve/configuration_builder.rb b/lib/mauve/configuration_builder.rb
index fb3c781..1d8d960 100644
--- a/lib/mauve/configuration_builder.rb
+++ b/lib/mauve/configuration_builder.rb
@@ -5,9 +5,11 @@ require 'mauve/people_list'
require 'mauve/source_list'
module Mauve
+ #
+ # This is the top-level configuration builder
+ #
class ConfigurationBuilder < ObjectBuilder
- #
# This overwrites the default ObjectBuilder initialize method, such that
# the context is set as a new configuration
#
@@ -16,19 +18,31 @@ module Mauve
# FIXME: need to test blocks that are not immediately evaluated
end
+ # Adds a source list
+ #
+ # @param [String] label
+ # @param [Array] list
+ #
+ # @return [Array] the whole source list for label
def source_list(label, *list)
_logger.warn "Duplicate source_list '#{label}'" if @result.source_lists.has_key?(label)
@result.source_lists[label] += list
end
+ # Adds a people list
+ #
+ # @param [String] label
+ # @param [Array] list
+ #
+ # @return [Array] the whole people list for label
def people_list(label, *list)
_logger.warn("Duplicate people_list '#{label}'") if @result.people_lists.has_key?(label)
@result.people_lists[label] += list
end
- #
# Have to use the method _logger here, cos logger is defined as a builder elsewhere.
#
+ # @return [Log4r::Logger]
def _logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
diff --git a/lib/mauve/configuration_builders.rb b/lib/mauve/configuration_builders.rb
index 03d666e..7ff6ac9 100644
--- a/lib/mauve/configuration_builders.rb
+++ b/lib/mauve/configuration_builders.rb
@@ -1,6 +1,15 @@
-
require 'mauve/configuration_builders/logger'
require 'mauve/configuration_builders/notification_method'
require 'mauve/configuration_builders/person'
require 'mauve/configuration_builders/server'
require 'mauve/configuration_builders/alert_group'
+
+module Mauve
+ #
+ # This is where all the builders for the various configuration stanzas are
+ # kept.
+ #
+ module ConfigurationBuilders
+
+ end
+end
diff --git a/lib/mauve/configuration_builders/alert_group.rb b/lib/mauve/configuration_builders/alert_group.rb
index 9561d4d..c652de8 100644
--- a/lib/mauve/configuration_builders/alert_group.rb
+++ b/lib/mauve/configuration_builders/alert_group.rb
@@ -7,59 +7,81 @@ require 'mauve/notification'
module Mauve
module ConfigurationBuilders
-
+ #
+ # This corresponds to a new "notify" clause an the alert_group config.
+ #
class Notification < 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")
+ # Sets up the notification
+ #
+ # @param [Array] who List of usernames or people_lists to notify
+ # @raise [ArgumentError] if a username doesn't exist.
+ #
+ # @return [Mauve::Notification] New notification instance.
+ 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 ArgumentError.new("You have not declared who #{username} is")
+ end
end
+ @result = Mauve::Notification.new(who, @context.last_alert_group.level)
end
- @result = Mauve::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 AlertGroup < ObjectBuilder
-
- def builder_setup(name=anonymous_name)
- @result = Mauve::AlertGroup.new(name)
- @context.last_alert_group = @result
+
+ is_attribute "every"
+ is_block_attribute "during"
end
- is_block_attribute "includes"
- is_attribute "acknowledgement_time"
- is_attribute "level"
+ # This corresponds to a new alert_group clause
+ #
+ class AlertGroup < ObjectBuilder
+
+ # Sets up the alert group, and sets the last_alert_group context.
+ #
+ # @param [String] name Name of the new alert group
+ #
+ # @return [Mauve::AlertGroup] New alert group instance
+ def builder_setup(name="anonymous_name")
+ @result = Mauve::AlertGroup.new(name)
+ @context.last_alert_group = @result
+ end
- is_builder "notify", Notification
+ is_block_attribute "includes"
+ is_attribute "acknowledgement_time"
+ is_attribute "level"
+ is_builder "notify", Mauve::ConfigurationBuilders::Notification
+
+ # Method called after the notify clause has been sorted. Adds new
+ # notification clause to the result.
+ #
+ # @param [Mauve::Notification] notification
+ #
+ def created_notify(notification)
+ @result.notifications ||= []
+ @result.notifications << notification
+ end
- def created_notify(notification)
- @result.notifications ||= []
- @result.notifications << notification
end
end
- end
-
- # this should live in AlertGroup but can't due to
+ # These constants define the levels available for alert groups.
+ #
+ # This should live in AlertGroup but can't due to
# http://briancarper.net/blog/ruby-instance_eval_constant_scoping_broken
#
module AlertGroupConstants
+ # Urgent level
URGENT = :urgent
+ # Normal level
NORMAL = :normal
+ # Low level
LOW = :low
end
@@ -70,9 +92,14 @@ module Mauve
is_builder "alert_group", ConfigurationBuilders::AlertGroup
+ # Called after an alert group is created. Checks to make sure that no more than one alert group shares a name.
+ #
+ # @param [Mauve::AlertGroup] alert_group The new AlertGroup
+ # @raise [ArgumentError] if an AlertGroup with the same name already exists.
+ #
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?
+ raise ArgumentError.new("Duplicate alert_group '#{name}'") unless @result.alert_groups.select { |g| g.name == name }.empty?
@result.alert_groups << alert_group
end
diff --git a/lib/mauve/configuration_builders/logger.rb b/lib/mauve/configuration_builders/logger.rb
index 3f60dfe..d757d88 100644
--- a/lib/mauve/configuration_builders/logger.rb
+++ b/lib/mauve/configuration_builders/logger.rb
@@ -7,31 +7,48 @@ module Mauve
class LoggerOutputter < ObjectBuilder
+ # Set up a Log4r::Outputter
+ #
+ # @param [String] outputter Outputter basic class name, like Stderr, Stdin, File.
+ #
def builder_setup(outputter)
@outputter = outputter.capitalize+"Outputter"
begin
Log4r.const_get(@outputter)
- rescue
+ rescue NameError
require "log4r/outputter/#{@outputter.downcase}"
end
- @outputter_name = "Mauve-"+5.times.collect{rand(36).to_s(36)}.join
+ @outputter_name = anonymous_name
@args = {}
end
+ # The new outputter
+ #
+ # @return [Log4r::Outputter]
def result
@result ||= Log4r.const_get(@outputter).new("Mauve", @args)
end
+ # Set the formatter for this outputter (see Log4r::PatternFormatter for
+ # allowed patterns). SyntaxError is caught in the ObjectBuilder#parse
+ # method.
+ #
+ # @param [String] f The format
+ #
+ # @return [Log4r::PatternFormatter]
def format(f)
result.formatter = Log4r::PatternFormatter.new(:pattern => f)
end
- #
# This is needed to be able to pass arbitrary arguments to Log4r
- # outputters.
+ # outputters. Missing methods / bad arguments are caught in the
+ # ObjectBuilder#parse method.
+ #
+ # @param [String] name
+ # @param [Object] value
#
def method_missing(name, value=nil)
if value.nil?
@@ -47,38 +64,59 @@ module Mauve
is_builder "outputter", LoggerOutputter
+ # Set up the new logger
+ #
def builder_setup
@result = Log4r::Logger['Mauve'] || Log4r::Logger.new('Mauve')
@default_format = nil
@default_level = Log4r::RootLogger.instance.level
end
+ # Set the default format. Syntax erros are caught in the
+ # ObjectBuilder#parse method.
+ #
+ # @param [String] f Any format pattern allowed by Log4r::PatternFormatter.
+ #
+ # @return [Log4r::PatternFormatter]
def default_format(f)
- begin
- @default_formatter = Log4r::PatternFormatter.new(:pattern => f)
- rescue SyntaxError
- raise BuildException.new "Bad log format #{f.inspect}"
- end
+ @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
+
+ @default_formatter
end
+ # Set the default log level.
+ #
+ # @param [Integer] l The log level.
+ # @raise [ArgumentError] If the log level is bad
+ #
+ # @return [Integer] The log level set.
def default_level(l)
if Log4r::Log4rTools.valid_level?(l)
@default_level = l
else
- raise "Bad default level set for the logger #{l}.inspect"
+ raise ArgumentError.new "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
+
+ @default_level
end
+ # This is called once an outputter has been created. It sets the default
+ # formatter and level, if these have been already set.
+ #
+ # @param [Log4r::Outputter] outputter Newly created outputter.
+ #
+ # @return [Log4r::Outputter] The adjusted outputter.
def created_outputter(outputter)
#
# Set the formatter and level for any newly created outputters
@@ -92,7 +130,6 @@ module Mauve
end
result.outputters << outputter
-# result.outputter.write("Created logger")
end
end
end
@@ -104,10 +141,15 @@ module Mauve
module LoggerConstants
Log4r.define_levels(*Log4r::Log4rConfig::LogLevels) # ensure levels are loaded
+ # Debug logging
DEBUG = Log4r::DEBUG
+ # Info logging
INFO = Log4r::INFO
+ # Warn logging
WARN = Log4r::WARN
+ # Error logging
ERROR = Log4r::ERROR
+ # Fatal logging
FATAL = Log4r::FATAL
end
diff --git a/lib/mauve/configuration_builders/notification_method.rb b/lib/mauve/configuration_builders/notification_method.rb
index 1192078..9596587 100644
--- a/lib/mauve/configuration_builders/notification_method.rb
+++ b/lib/mauve/configuration_builders/notification_method.rb
@@ -1,27 +1,46 @@
require 'mauve/notifiers'
require 'mauve/configuration_builder'
-# encoding: UTF-8
module Mauve
module ConfigurationBuilders
class NotificationMethod < ObjectBuilder
+ #
+ # Set up the notification. Missing notifiers are caught via NameError in
+ # the ObjectBuilder#parse method.
+ #
+ # @param [String] name Name of the notifier
+ #
def builder_setup(name)
- @notification_type = name.capitalize
+ notifiers_base = Mauve::Notifiers
+
+ @notifier_type = notifiers_base.const_get(name.capitalize)
+
@name = name
provider("Default")
end
+ # This allows use of multiple notification providers, e.g. in the case of
+ # SMS.
+ #
+ # Missing providers are caught via NameError in the ObjectBuilder#parse
+ # method.
+ #
def provider(name)
- notifiers_base = Mauve::Notifiers
- notifiers_type = notifiers_base.const_get(@notification_type)
- @provider_class = notifiers_type.const_get(name)
+ @provider_class = @notifier_type.const_get(name)
end
+ # Returns the result for this builder, depending on the configuration
+ #
def result
@result ||= @provider_class.new(@name)
end
-
+
+ # This catches all methods available for a provider, as needed.
+ #
+ # Missing methods / bad arguments etc. are caught in the
+ # ObjectBuilder#parse method, via NoMethodError.
+ #
def method_missing(name, value=nil)
if value
result.send("#{name}=".to_sym, value)
@@ -38,6 +57,9 @@ module Mauve
class ConfigurationBuilder < ObjectBuilder
is_builder "notification_method", ConfigurationBuilders::NotificationMethod
+ # Method called after a notification method has been created to check for duplicate names.
+ #
+ # @raise [BuildException] when a duplicate notification method is found.
def created_notification_method(notification_method)
name = notification_method.name
raise BuildException.new("Duplicate notification '#{name}'") if @result.notification_methods[name]
diff --git a/lib/mauve/configuration_builders/person.rb b/lib/mauve/configuration_builders/person.rb
index 1c012f2..c5314c5 100644
--- a/lib/mauve/configuration_builders/person.rb
+++ b/lib/mauve/configuration_builders/person.rb
@@ -15,29 +15,21 @@ module Mauve
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.to_s
- end
+ is_attribute "password"
+ is_attribute "sms"
+ is_attribute "holiday_url"
+ is_attribute "email"
+ is_attribute "xmpp"
+ is_attribute "sms"
- def email(e)
- @result.email = e.to_s
- end
+ # Sets the block for all levels of alert
+ #
+ # @param [Block] block
+ def all(&block); urgent(&block); normal(&block); low(&block); end
- def xmpp(x)
- @result.xmpp = x.to_s
- end
-
- def sms(x)
- @result.sms = x.to_s
- end
-
+ # Notification suppression hash
+ #
+ # @param [Hash] h
def suppress_notifications_after(h)
raise ArgumentError.new("notification_threshold must be specified as e.g. (10 => 1.minute)") unless h.kind_of?(Hash)
@@ -54,9 +46,14 @@ module Mauve
is_builder "person", ConfigurationBuilders::Person
+ # Method called once a person has been created to check for duplicate names
+ #
+ # @param [Mauve::Person] person
+ # @raise [ArgumentError] if a person has already been declared.
+ #
def created_person(person)
name = person.username
- raise BuildException.new("Duplicate person '#{name}'") if @result.people[name]
+ raise ArgumentError.new("Duplicate person '#{name}'") if @result.people[name]
#
# Add a default notification threshold
#
diff --git a/lib/mauve/configuration_builders/server.rb b/lib/mauve/configuration_builders/server.rb
index 3a9f5ec..c000144 100644
--- a/lib/mauve/configuration_builders/server.rb
+++ b/lib/mauve/configuration_builders/server.rb
@@ -5,74 +5,157 @@ require 'mauve/configuration_builder'
module Mauve
module ConfigurationBuilders
+ #
+ # This is the HTTP server
+ #
class HTTPServer < ObjectBuilder
+ # The port the http server listens on
is_attribute "port"
+ # The IP address the http server listens on. IPv6 is *NOT* OK.
is_attribute "ip"
+ # Where all the templates are kept
is_attribute "document_root"
+ # The secret for the cookies
is_attribute "session_secret"
+ # The base URL of the server.
is_attribute "base_url"
+ # Sets up a Mauve::HTTPServer singleton as the result
+ #
+ # @return [Mauve::HTTPServer]
def builder_setup
@result = Mauve::HTTPServer.instance
end
end
+ #
+ # This is the UDP server.
+ #
class UDPServer < ObjectBuilder
+ # This is the port the server listens on
is_attribute "port"
+ # This is the IP address the server listens on. IPv6 is OK! e.g. [::] for all addresses
is_attribute "ip"
+ # This is the sleep interval for the UDP server.
is_attribute "poll_every"
+ # Sets up a Mauve::UDPServer singleton as the result
+ #
+ # @return [Mauve::UDPServer]
def builder_setup
@result = Mauve::UDPServer.instance
end
end
+ #
+ # This is the thread that pulls packets from the queue for processing.
+ #
class Processor < ObjectBuilder
+ # This is the interval between polls of the packet queue.
is_attribute "poll_every"
+ # This is the timeout for the transmission cache, which allows duplicate packets to be discarded.
is_attribute "transmission_cache_expire_time"
+ # Sets up a Mauve::Processor singleton as the result
+ #
+ # @return [Mauve::Processor]
def builder_setup
@result = Mauve::Processor.instance
end
end
+ #
+ # This is the Timer singleton.
+ #
class Timer < ObjectBuilder
+ #
+ # This is the interval at which the Timer thread is run. This will limit
+ # the rate at which notifications can be sent, if set.
+ #
is_attribute "poll_every"
+ # Sets up a Mauve::Timer singleton as the result
+ #
+ # @return [Mauve::Timer]
def builder_setup
@result = Mauve::Timer.instance
end
end
class Notifier < ObjectBuilder
+ #
+ # This is the interval at which the notification queue is polled for new
+ # notifications to be sent. This will not have any rate-limiting effect.
+ #
is_attribute "poll_every"
+ # Sets up a Mauve::Notifier singleton as the result
+ #
+ # @return [Mauve::Notifier]
def builder_setup
@result = Mauve::Notifier.instance
end
end
+ #
+ # This sends a mauve heartbeat to another Mauve instance elsewhere
+ #
class Heartbeat < ObjectBuilder
+ #
+ # The destination for the heartbeat
+ #
is_attribute "destination"
+
+ #
+ # The detail field for the heartbeat
+ #
is_attribute "detail"
+
+ #
+ # The summary field for the heartbeat.
+ #
is_attribute "summary"
+
+ #
+ # How long to raise an alert after the last heartbeat.
+ #
is_attribute "raise_after"
+
+ #
+ # The interval between heartbeats
+ #
is_attribute "send_every"
-
+
+ # Sets up a Mauve::Heartbeat singleton as the result
+ #
+ # @return [Mauve::Heartbeat]
def builder_setup
@result = Mauve::Heartbeat.instance
end
end
class Pop3Server < ObjectBuilder
+ #
+ # The IP adderess the Pop3 server listens on
+ #
is_attribute "ip"
+
+ #
+ # The POP3 server port
+ #
is_attribute "port"
+ # Sets up a Mauve::Pop3Server singleton as the result
+ #
+ # @return [Mauve::Pop3Server]
def builder_setup
@result = Mauve::Pop3Server.instance
end
end
+ #
+ # This is the main Server singleton.
+ #
class Server < ObjectBuilder
#
# Set up second-level builders
@@ -85,8 +168,19 @@ module Mauve
is_builder "heartbeat", Heartbeat
is_builder "pop3_server", Pop3Server
+ #
+ # The name of the server this instance of Mauve is running on
+ #
is_attribute "hostname"
+
+ #
+ # The database definition
+ #
is_attribute "database"
+
+ #
+ # The period of sleep during which no heartbeats are raised.
+ #
is_attribute "initial_sleep"
def builder_setup
@@ -102,8 +196,12 @@ module Mauve
is_builder "server", ConfigurationBuilders::Server
+ # This is called once the server object has been created.
+ #
+ # @raise [SyntaxError] if more than one server clause has been defined.
+ #
def created_server(server)
- raise BuildError.new("Only one 'server' clause can be specified") if @result.server
+ raise SyntaxError.new("Only one 'server' clause can be specified") if @result.server
@result.server = server
end
diff --git a/lib/mauve/heartbeat.rb b/lib/mauve/heartbeat.rb
index 39d3499..1fe2fab 100644
--- a/lib/mauve/heartbeat.rb
+++ b/lib/mauve/heartbeat.rb
@@ -3,17 +3,23 @@ require 'mauve/proto'
require 'mauve/mauve_thread'
require 'log4r'
-#
-# This class is responsible for sending a heartbeat to another mauve instance elsewhere.
-#
module Mauve
+ #
+ # This class is responsible for sending a heartbeat to another mauve instance elsewhere.
+ #
class Heartbeat < MauveThread
include Singleton
+ #
+ # Allow access to some basics.
+ #
attr_reader :raise_after, :destination, :summary, :detail
+ #
+ # This sets up the Heartbeat singleton
+ #
def initialize
super
@@ -24,6 +30,10 @@ module Mauve
@poll_every = 60
end
+ #
+ # This is the time period after which an alert is raised by the remote Mauve instance.
+ # @param [Integer] i Seconds
+ # @return [Integer] Seconds
def raise_after=(i)
raise ArgumentError "raise_after must be an integer" unless i.is_a?(Integer)
@raise_after = i
@@ -31,25 +41,39 @@ module Mauve
alias send_every= poll_every=
+ # Sets the summary of the heartbeat
+ #
+ # @param [String] s Summary
def summary=(s)
raise ArgumentError "summary must be a string" unless s.is_a?(String)
@summary = s
end
+ # Sets the detail of the heartbeat
+ #
+ # @param [String] d Detail
def detail=(d)
raise ArgumentError "detail must be a string" unless d.is_a?(String)
@detail = d
end
+ # Sets the destinantion Mauve instance
+ #
+ # @param [String] d Destination
+ #
def destination=(d)
raise ArgumentError "destination must be a string" unless d.is_a?(String)
@destination = d
end
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
+ private
+
+ # @private This is the main heartbeat loop.
def main_loop
#
# Don't send if no destination set.
diff --git a/lib/mauve/history.rb b/lib/mauve/history.rb
index 76025a0..d9ab8e9 100644
--- a/lib/mauve/history.rb
+++ b/lib/mauve/history.rb
@@ -4,6 +4,10 @@ require 'mauve/alert'
require 'log4r'
module Mauve
+ # This is the look-up table for Alerts and History to allow one History to
+ # have many Alerts and vice-versa
+ #
+ #
class AlertHistory
include DataMapper::Resource
@@ -15,6 +19,10 @@ module Mauve
after :destroy, :remove_unreferenced_histories
+ # This is a horid migration to allow a move from an older version of Mauve
+ # without this table.
+ #
+ #
def self.migrate!
#
# This copies the alert IDs from the old History table to the new AlertHistories thing, but only if there are no AertHistories
@@ -45,12 +53,20 @@ module Mauve
private
+ # This just removes histories that this AlertHistory used to refer to, if
+ # they have no other alerts associated with them
+ #
+ #
def remove_unreferenced_histories
self.history.destroy unless self.history.alerts.count > 0
end
end
+ # This class keeps a history for Mauve. One History can relate to zero or
+ # more Alerts, allowing notes to be added.
+ #
+ #
class History
include DataMapper::Resource
@@ -73,7 +89,6 @@ module Mauve
protected
- #
# This cleans the HTML before saving.
#
def do_sanitize_html
@@ -93,16 +108,21 @@ module Mauve
end
end
-
+ # Update the created_at time on the object
+ #
def set_created_at(context = :default)
self.created_at = Time.now unless self.created_at.is_a?(Time)
end
public
+ # This adds an alert or an array of alerts to the cache of alerts
+ # associated with this model.
#
# Blasted datamapper not eager-loading my model.
#
+ # @param [Array or Alert] a Array of Alerts or a single Alert
+ # @raise ArgumentError If +a+ is not an Array or an Alert
def add_to_cached_alerts(a)
@cached_alerts ||= []
if a.is_a?(Array) and a.all?{|m| m.is_a?(Alert)}
@@ -114,15 +134,22 @@ module Mauve
end
end
+ # Find all the alerts for this History. This caches the alerts found.
+ # Call #reload to get rid of the cache.
+ #
+ # @return [Array] Alerts
def alerts
@cached_alerts ||= super
end
+ # Reload the object, and clear the cache.
+ #
def reload
@cached_alerts = nil
super
end
+ # @return Log4r::Logger
def logger
Log4r::Logger.new self.class.to_s
end
diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb
index 72eb85a..583e6b5 100644
--- a/lib/mauve/http_server.rb
+++ b/lib/mauve/http_server.rb
@@ -21,26 +21,41 @@ require 'rack/handler/webrick'
# Bodge up thin logging.
#
module Thin
+ #
+ # Bodge up thin logging.
+ #
module Logging
-
+
+ # Log a message at "info" level
+ #
+ # @param [String] m
def log(m=nil)
# return if Logging.silent?
logger = Log4r::Logger.new "Mauve::HTTPServer"
logger.info(m || yield)
end
+ # Log a message at "debug" level
+ #
+ # @param [String] m
def debug(m=nil)
# return unless Logging.debug?
logger = Log4r::Logger.new "Mauve::HTTPServer"
logger.debug(m || yield)
end
+ # Log a trace at "debug" level
+ #
+ # @param [String] m
def trace(m=nil)
return unless Logging.trace?
logger = Log4r::Logger.new "Mauve::HTTPServer"
logger.debug(m || yield)
end
+ # Log a message at "error" level
+ #
+ # @param [String] e
def log_error(e=$!)
logger = Log4r::Logger.new "Mauve::HTTPServer"
logger.error(e)
@@ -58,8 +73,16 @@ end
#
class RackErrorsProxy
+ #
+ # Set up the instance
+ #
+ # @param [Log4r::Logger] l The logger instance.
+ #
def initialize(l); @logger = l; end
+ # Log a message at "error" level
+ #
+ # @param [String or Array] msg
def write(msg)
case msg
when String then @logger.info(msg.chomp)
@@ -72,6 +95,8 @@ class RackErrorsProxy
alias_method :<<, :write
alias_method :puts, :write
+ # no-op
+ #
def flush; end
end
@@ -81,7 +106,7 @@ end
module Mauve
#
- # API to control the web server
+ # The HTTP Server object
#
class HTTPServer < MauveThread
@@ -90,6 +115,9 @@ module Mauve
attr_reader :port, :ip, :document_root, :base_url
attr_reader :session_secret
+ #
+ # Initialze the server
+ #
def initialize
super
self.port = 1288
@@ -98,11 +126,18 @@ module Mauve
self.session_secret = "%x" % rand(2**100)
end
+ # Set the port
+ #
+ # @param [Intger] pr The port number between 1 and 2**16-1
+ # @raise [ArgumentError] If the port is not valid
def port=(pr)
- raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
+ raise ArgumentError, "port must be an integer between 1 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
@port = pr
end
+ # Set the listening IP address
+ #
+ # @param [String] i The IP
def ip=(i)
raise ArgumentError, "ip must be a string" unless i.is_a?(String)
#
@@ -112,6 +147,12 @@ module Mauve
@ip = i
end
+ # Set the document root.
+ # @param [String] d The directory where the templates etc are kept.
+ # @raise [ArgumentError] If d is not a string
+ # @raise [Errno::ENOTDIR] If d does not exist
+ # @raise [Errno::ENOTDIR] If d is not a directory
+ #
def document_root=(d)
raise ArgumentError, "document_root must be a string" unless d.is_a?(String)
raise Errno::ENOENT, d unless File.exists?(d)
@@ -120,6 +161,10 @@ module Mauve
@document_root = d
end
+ # Set the base URL
+ #
+ # @param [String] b The base URL, including https?://
+ # @raise [ArgumentError] If b is not a string, or https?:// is missing
def base_url=(b)
raise ArgumentError, "base_url must be a string" unless b.is_a?(String)
raise ArgumentError, "base_url should start with http:// or https://" unless b =~ /^https?:\/\//
@@ -128,35 +173,50 @@ module Mauve
#
@base_url = b.chomp("/")
end
-
+
+ # Set the cookie session secret
+ #
+ # @param [String] s The secret
+ # @raise [ArgumentError] if s is not a string
def session_secret=(s)
raise ArgumentError, "session_secret must be a string" unless s.is_a?(String)
@session_secret = s
end
- def main_loop
- unless @server and @server.running?
- #
- # Sessions are kept for 8 days.
- #
- @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 8.weeks}), :signals => false)
- @server.start
- end
- end
-
+ # Return the base_url
+ #
+ # @return [String]
def base_url
@base_url ||= "http://"+Server.instance.hostname
end
+ # Stop the server
+ #
def stop
@server.stop if @server and @server.running?
super
end
+ # Stop the server, faster than #stop
+ #
def join
@server.stop! if @server and @server.running?
super
end
+ private
+
+ #
+ # @private This is the main loop to keep the server going.
+ #
+ def main_loop
+ unless @server and @server.running?
+ #
+ # Sessions are kept for 8 days.
+ #
+ @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 8.weeks}), :signals => false)
+ @server.start
+ end
+ end
end
end
diff --git a/lib/mauve/mauve_resolv.rb b/lib/mauve/mauve_resolv.rb
index 6c97bef..c6460e3 100644
--- a/lib/mauve/mauve_resolv.rb
+++ b/lib/mauve/mauve_resolv.rb
@@ -1,12 +1,19 @@
require 'resolv-replace'
-#
-#
-#
-
module Mauve
+ #
+ # This is just a quick class to resolve a hostname to all its IPs, IPv6 and IPv4.
+ #
class MauveResolv
+
class << self
+
+ # Get all IPs for a host, both IPv6 and IPv4. ResolvError and
+ # ResolvTimeout are both rescued.
+ #
+ # @param [String] host The hostname
+ # @return [Array] Array of IP addresses, as Strings.
+ #
def get_ips_for(host)
record_types = %w(A AAAA)
ips = []
@@ -25,6 +32,7 @@ module Mauve
ips
end
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.to_s)
end
diff --git a/lib/mauve/mauve_thread.rb b/lib/mauve/mauve_thread.rb
index 2191d58..42f41a2 100644
--- a/lib/mauve/mauve_thread.rb
+++ b/lib/mauve/mauve_thread.rb
@@ -3,18 +3,39 @@ require 'singleton'
module Mauve
+ #
+ # This is a class to wrap our threads that have processing loops.
+ #
+ # The thread is kept in a wrapper to allow it to be frozen and thawed at
+ # convenient times.
+ #
class MauveThread
+ #
+ # The sleep interval between runs of the main loop. Defaults to 5 seconds.
+ #
attr_reader :poll_every
+ #
+ # Set the thread up
+ #
def initialize
@thread = nil
end
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
+ # Set the sleep interval between runs of the main loop. This can be
+ # anything greater than or equal to zero. If a number less than zero gets
+ # entered, it will be increased to zero.
+ #
+ # @param [Numeric] i The number of seconds to sleep
+ # @raise [ArgumentError] If +i+ is not numeric
+ # @return [Numeric]
+ #
def poll_every=(i)
raise ArgumentError.new("poll_every must be numeric") unless i.is_a?(Numeric)
#
@@ -28,60 +49,20 @@ module Mauve
@poll_every = i
end
- def run_thread(interval = 5.0)
- #
- # Good to go.
- #
- @thread = Thread.current
- self.state = :starting
-
- @poll_every ||= interval
- #
- # Make sure we get a number.
- #
- @poll_every = 5 unless @poll_every.is_a?(Numeric)
-
- rate_limit = 0.1
-
- while self.state != :stopping do
-
- self.state = :started if self.state == :starting
-
- #
- # Schtop!
- #
- if self.state == :freezing
- self.state = :frozen
- Thread.stop
- self.state = :started
- end
-
- yield_start = Time.now.to_f
-
- yield
-
- #
- # Ah-ha! Sleep with a break clause. Make sure we poll every @poll_every seconds.
- #
- ((@poll_every.to_f - Time.now.to_f + yield_start.to_f)/rate_limit).
- round.to_i.times do
-
- break if self.should_stop?
-
- #
- # This is a rate-limiting step
- #
- Kernel.sleep rate_limit
- end
- end
-
- self.state = :stopped
- end
-
+ # This determines if a thread should stop
+ #
+ # @return [Boolean]
def should_stop?
[:freezing, :stopping].include?(self.state)
end
+ # This is the current state of the thread. It can be one of
+ # [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed]
+ #
+ # If the thread is not alive it will be +:stopped+.
+ #
+ # @return [Symbol] One of [:stopped, :starting, :started, :freezing,
+ # :frozen, :stopping, :killing, :killed]
def state
if self.alive?
@thread.key?(:state) ? @thread[:state] : :unknown
@@ -90,9 +71,18 @@ module Mauve
end
end
+ # This sets the state of a thread. It also records the last time the
+ # thread changed status.
+ #
+ # @param [Symbol] s One of [:stopped, :starting, :started, :freezing,
+ # :frozen, :stopping, :killing, :killed]
+ # @raise [ArgumentError] if +s+ is not a valid symbol or the thread is not
+ # ready
+ # @return [Symbol] the current thread state.
+ #
def state=(s)
- raise "Bad state for mauve_thread #{s.inspect}" unless [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed].include?(s)
- raise "Thread not ready yet." unless @thread.is_a?(Thread)
+ raise ArgumentError, "Bad state for mauve_thread #{s.inspect}" unless [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed].include?(s)
+ raise ArgumentError, "Thread not ready yet." unless @thread.is_a?(Thread)
unless @thread[:state] == s
@thread[:state] = s
@@ -103,6 +93,9 @@ module Mauve
@thread[:state]
end
+ # This returns the time of the last state change, or nil if the thread is dead.
+ #
+ # @return [Time or Nilclass]
def last_state_change
if self.alive? and @thread.key?(:last_state_change)
return @thread[:last_state_change]
@@ -111,6 +104,8 @@ module Mauve
end
end
+ # This asks the thread to freeze at the next opportunity.
+ #
def freeze
self.state = :freezing
@@ -119,10 +114,16 @@ module Mauve
logger.warn("Thread has not frozen!") unless @thread.stop?
end
+ # This returns true if the thread has frozen successfully.
+ #
+ # @return [Boolean]
def frozen?
self.stop? and self.state == :frozen
end
+ # This starts the thread. It wakes it up if it is alive, or starts it from
+ # fresh if it is dead.
+ #
def run
if self.alive?
# Wake up if we're stopped.
@@ -132,7 +133,7 @@ module Mauve
else
@logger = nil
Thread.new do
- self.run_thread { self.main_loop }
+ run_thread { main_loop }
end
end
end
@@ -140,15 +141,23 @@ module Mauve
alias start run
alias thaw run
+ # This checks to see if the thread is alive
+ #
+ # @return [Boolean]
def alive?
@thread.is_a?(Thread) and @thread.alive?
end
+ # This checks to see if the thread is stopped
+ #
+ # @return [Boolean]
def stop?
self.alive? and @thread.stop?
end
- def join(ok_exceptions=[])
+ # This joins the thread
+ #
+ def join
@thread.join if @thread.is_a?(Thread)
end
@@ -156,15 +165,24 @@ module Mauve
# @thread.raise(ex)
# end
+ # This returns the thread's backtrace
+ #
+ # @return [Array or Nilclass]
def backtrace
@thread.backtrace if @thread.is_a?(Thread)
end
+ # This restarts the thread
+ #
+ #
def restart
self.stop
self.start
end
+ # This stops the thread
+ #
+ #
def stop
self.state = :stopping
@@ -183,16 +201,79 @@ module Mauve
alias exit stop
+ # This kills the thread -- faster than #stop
+ #
def kill
self.state = :killing
@thread.kill
self.state = :killed
end
+ # This returns the thread itself.
+ #
+ # @return [Thread]
def thread
@thread
end
+
+ private
+
+ # This is the main run loop for the thread. In here are all the calls
+ # allowing use to freeze and thaw the thread neatly.
+ #
+ # This thread will run untill the thread state is changed to :stopping.
+ #
+ def run_thread(interval = 5.0)
+ #
+ # Good to go.
+ #
+ @thread = Thread.current
+ self.state = :starting
+
+ @poll_every ||= interval
+ #
+ # Make sure we get a number.
+ #
+ @poll_every = 5 unless @poll_every.is_a?(Numeric)
+
+ rate_limit = 0.1
+
+ while self.state != :stopping do
+
+ self.state = :started if self.state == :starting
+
+ #
+ # Schtop!
+ #
+ if self.state == :freezing
+ self.state = :frozen
+ Thread.stop
+ self.state = :started
+ end
+
+ yield_start = Time.now.to_f
+
+ yield
+
+ #
+ # Ah-ha! Sleep with a break clause. Make sure we poll every @poll_every seconds.
+ #
+ ((@poll_every.to_f - Time.now.to_f + yield_start.to_f)/rate_limit).
+ round.to_i.times do
+
+ break if self.should_stop?
+
+ #
+ # This is a rate-limiting step
+ #
+ Kernel.sleep rate_limit
+ end
+ end
+
+ self.state = :stopped
+ end
+
end
end
diff --git a/lib/mauve/mauve_time.rb b/lib/mauve/mauve_time.rb
index 2d9e293..d2949d9 100644
--- a/lib/mauve/mauve_time.rb
+++ b/lib/mauve/mauve_time.rb
@@ -1,14 +1,19 @@
require 'date'
require 'time'
-
-# Seconds, minutes, hours, days, and weeks... More than that, we
-# really should not need it.
+#
+# Extra methods for integer to calculate periods in seconds.
+#
class Integer
+ # @return [Integer]
def seconds; self; end
+ # @return [Integer] n minutes of seconds
def minutes; self*60; end
+ # @return [Integer] n hours of seconds
def hours; self*3600; end
+ # @return [Integer] n days of seconds
def days; self*86400; end
+ # @return [Integer] n weeks of seconds
def weeks; self*604800; end
alias_method :day, :days
alias_method :hour, :hours
@@ -16,36 +21,22 @@ class Integer
alias_method :week, :weeks
end
-class Date
- def to_time
- Time.parse(self.to_s)
- end
-end
-
-class DateTime
- def to_time
- Time.parse(self.to_s)
- end
-
- def to_s_relative(*args)
- self.to_s_relative(*args)
- end
-
- def to_s_human
- self.to_s_human
- end
-
- def in_x_hours(*args)
- self.in_x_hours(*args)
- end
-
-end
-
+#
+# Extra methods for Time.
+#
class Time
+ #
+ # Method to work out when something is due, with different classes of hours.
+ #
+ # @param [Integer] n The number of hours in the future
+ # @param [String] type One of wallclock, working, or daytime.
+ #
+ # @return [Time]
+ #
def in_x_hours(n, type="wallclock")
t = self.dup
#
- # Do this in minutes rather than hours
+ # Do this in seconds rather than hours
#
n = n.to_i*3600
@@ -103,34 +94,42 @@ class Time
t
end
+ # Test to see if we're in working hours. The working day is from 8.30am until
+ # 17:00
#
- # The working day is from 8.30am until 17:00
- #
+ # @return [Boolean]
def working_hours?
(1..5).include?(self.wday) and ((9..16).include?(self.hour) or (self.hour == 8 && self.min >= 30))
end
+ # Test to see if it is currently daytime. The daytime day is 14 hours long
#
- # The daytime day is 14 hours long
- #
+ # @return [Boolean]
def daytime_hours?
(8..21).include?(self.hour)
end
+
+ # We're always in wallclock hours
#
- # The daytime day is 14 hours long
- #
+ # @return [true]
def wallclock_hours?
true
end
+ # Test to see if we're in the DEAD ZONE! This is from 3 - 6am every day.
#
- # In the DEAD ZONE!
- #
+ # @return [Boolean]
def dead_zone?
(3..6).include?(self.hour)
end
-
+
+ # Format the time as a string, relative to +now+
+ #
+ # @param [Time] now The time we're using as a base
+ # @raise [ArgumentError] if +now+ is not a Time
+ # @return [String]
+ #
def to_s_relative(now = Time.now)
#
# Make sure now is the correct class
diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb
index 00ce989..920f1b1 100644
--- a/lib/mauve/notification.rb
+++ b/lib/mauve/notification.rb
@@ -6,7 +6,7 @@ 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
+ # \@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:
@@ -15,7 +15,7 @@ module Mauve
#
# ... later on ...
#
- # DuringRunner.new(Time.now, my_alert, &during).inside?
+ # DuringRunner.new(Time.now, my_alert, &during).now?
#
# ... or to ask when an alert will next be cued ...
#
@@ -28,6 +28,14 @@ module Mauve
attr_reader :time, :alert, :during
+ #
+ # Sets up the class
+ #
+ # @param [Time] time The time we're going to test
+ # @param [Mauve::Alert or Nilclass] alert The alert that we can use in tests.
+ # @param [Proc] during The Proc that is evaluated inside the instance to decide if now is now!
+ # @raise [ArgumentError] if +time+ is not a Time
+ #
def initialize(time, alert=nil, &during)
raise ArgumentError.new("Must supply time (not #{time.inspect})") unless time.is_a?(Time)
@time = time
@@ -36,14 +44,21 @@ module Mauve
@logger = Log4r::Logger.new "Mauve::DuringRunner"
end
+ # This evaluates the +during+ block, returning the result.
#
- #
- #
+ # @param [Time] t Set the time at which to evaluate the +during+ block.
+ # @return [Boolean]
def now?(t=@time)
@test_time = t
- res = instance_eval(&@during)
+ instance_eval(&@during) ? true : false
end
+ # This finds the next occurance of the +during+ block evaluating to true.
+ # It returns nil if an occurence cannot be found within the next 8 days.
+ #
+ # @param [Integer] after Skip time forward this many seconds before starting
+ # @return [Time or nil]
+ #
def find_next(after = 0)
t = @time+after
#
@@ -86,18 +101,28 @@ module Mauve
protected
+ # Returns true if the current hour is in the list of hours given.
+ #
+ # @param [Array] hours List of hours (as Integers)
+ # @return [Boolean]
def hours_in_day(*hours)
@test_time = @time if @test_time.nil?
x_in_list_of_y(@test_time.hour, hours.flatten)
- end
-
+ end
+
+ # Returns true if the current day is in the list of days given
+ #
+ # @param [Array] days List of days (as Integers)
+ # @return [Boolean]
def days_in_week(*days)
@test_time = @time if @test_time.nil?
x_in_list_of_y(@test_time.wday, days.flatten)
end
- ## Return true if the alert has not been acknowledged within a certain time.
- #
+ # Tests if the alert has not been acknowledged within a certain time.
+ #
+ # @param [Integer] seconds Number of seconds
+ # @return [Boolean]
def unacknowledged(seconds)
@test_time = @time if @test_time.nil?
@alert &&
@@ -106,6 +131,11 @@ module Mauve
(@test_time - @alert.raised_at) >= seconds
end
+ # Checks to see if x is contained in y
+ #
+ # @param [Array] y Array to search for +x+
+ # @param [Object] x
+ # @return [Boolean]
def x_in_list_of_y(x,y)
y.any? do |range|
if range.respond_to?("include?")
@@ -116,16 +146,17 @@ module Mauve
end
end
+ # Test to see if we're in working hours. See Time#working_hours?
+ #
+ # @return [Boolean]
def working_hours?
@test_time = @time if @test_time.nil?
@test_time.working_hours?
end
- # Return true in the dead zone between 3 and 7 in the morning.
+ # Test to see if we're in the dead zone. See Time#dead_zone?
#
- # 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.
+ # @return [Boolean]
def dead_zone?
@test_time = @time if @test_time.nil?
@test_time.dead_zone?
@@ -139,20 +170,31 @@ module Mauve
#
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
-
+ # Set up a new notification
+ #
+ # @param [Array] people List of Mauve::Person to notify
+ # @param [Symbol] level Level at which to notify
def initialize(people, level)
self.level = level
self.every = 300
self.people = people
end
+ # @return [String]
+ def to_s
+ "#<Notification:of #{people.map { |p| p.username }.join(',')} at level #{level} every #{every}>"
+ end
+
+ # @return Log4r::Logger
def logger ; Log4r::Logger.new self.class.to_s ; end
+ # Push a notification on to the queue for this alert. The Mauve::Notifier
+ # will then pop it off and do the notification in a separate thread.
+ #
+ # @param [Mauve::Alert or Mauve::AlertChanged] alert The alert in question
+ # @param [Array] already_sent_to A list of people that have already received this alert.
+ #
+ # @return [Array] The list of people that have received this alert.
def notify(alert, already_sent_to = [])
if people.nil? or people.empty?
@@ -187,7 +229,13 @@ module Mauve
return already_sent_to
end
-
+
+ # Work out when this notification should next get sent. Nil will be
+ # returned if the alert is not raised.
+ #
+ # @param [Mauve::Alert] alert The alert in question
+ # @return [Time or nil] The time a reminder should get sent, or nil if it
+ # should never get sent again.
def remind_at_next(alert)
return nil unless alert.raised?
diff --git a/lib/mauve/notifier.rb b/lib/mauve/notifier.rb
index 2853485..14e188c 100644
--- a/lib/mauve/notifier.rb
+++ b/lib/mauve/notifier.rb
@@ -4,33 +4,45 @@ require 'mauve/notifiers/xmpp'
module Mauve
+ # The Notifier is reponsible for popping notifications off the
+ # notification_buffer run by the Mauve::Server instance. This ensures that
+ # notifications are sent in a separate thread to the main processing /
+ # updating threads, and stops notifications delaying updates.
+ #
+ #
class Notifier < MauveThread
include Singleton
- def main_loop
- #
- # Cycle through the buffer.
- #
- sz = Server.notification_buffer_size
+ # Stop the notifier thread. This just makes sure that all the
+ # notifications in the buffer have been sent before closing the XMPP
+ # connection.
+ #
+ def stop
+ super
- # Thread.current[:notification_threads] ||= []
- logger.info "Sending #{sz} alerts" if sz > 0
-
- sz.times do
- person, *args = Server.notification_pop
-
- #
- # Nil person.. that's craaazy too!
- #
- next if person.nil?
+ #
+ # Flush the queue.
+ #
+ main_loop
- person.send_alert(*args)
+ if Configuration.current.notification_methods['xmpp']
+ Configuration.current.notification_methods['xmpp'].close
end
+
end
- def start
- if Configuration.current.notification_methods['xmpp']
+ private
+
+ # This is the main loop that is executed in the thread.
+ #
+ #
+ def main_loop
+
+ #
+ # Make sure we're connected to the XMPP server if needed on every iteration.
+ #
+ if Configuration.current.notification_methods['xmpp'] and !Configuration.current.notification_methods['xmpp'].ready?
#
# Connect to XMPP server
#
@@ -41,37 +53,48 @@ module Mauve
#
# Ignore people without XMPP stanzas.
#
- next unless person.xmpp
+ next unless person.xmpp
+
+ #
+ # Can't do this unless we're ready.
+ #
+ next unless xmpp.ready?
#
# For each JID, either ensure they're on our roster, or that we're in
# that chat room.
#
jid = if xmpp.is_muc?(person.xmpp)
- xmpp.join_muc(person.xmpp)
+ xmpp.join_muc(person.xmpp)
else
xmpp.ensure_roster_and_subscription!(person.xmpp)
end
Configuration.current.people[username].xmpp = jid unless jid.nil?
end
+
end
- super
- end
+ #
+ # Cycle through the buffer.
+ #
+ sz = Server.notification_buffer_size
- def stop
- super
+ logger.info "Sending #{sz} alerts" if sz > 0
#
- # Flush the queue.
+ # Empty the buffer, one notification at a time.
#
- main_loop
+ sz.times do
+ person, *args = Server.notification_pop
+
+ #
+ # Nil person.. that's craaazy too!
+ #
+ next if person.nil?
- if Configuration.current.notification_methods['xmpp']
- Configuration.current.notification_methods['xmpp'].close
+ person.send_alert(*args)
end
-
end
end
diff --git a/lib/mauve/notifiers.rb b/lib/mauve/notifiers.rb
index 7ba1d71..7276091 100644
--- a/lib/mauve/notifiers.rb
+++ b/lib/mauve/notifiers.rb
@@ -1,6 +1,13 @@
-# encoding: UTF-8
require 'mauve/notifiers/email'
require 'mauve/notifiers/sms_default'
require 'mauve/notifiers/sms_aql'
require 'mauve/notifiers/xmpp'
+module Mauve
+ #
+ # This is where the various notifiers are kept.
+ #
+ module Notifiers
+
+ end
+end
diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb
index 3134135..8efdbec 100644
--- a/lib/mauve/notifiers/email.rb
+++ b/lib/mauve/notifiers/email.rb
@@ -30,10 +30,14 @@ module Mauve
def logger
@logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"")
-
end
def send_alert(destination, alert, all_alerts, conditions = {})
+
+ logger.debug [destination, alert, all_alerts, conditions]
+
+ return false if destination.nil?
+
message = prepare_message(destination, alert, all_alerts, conditions)
args = [@server, @port]
args += [@username, @password, @login_method.to_sym] if @login_method
diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb
index 5ffe8f4..ae9734f 100644
--- a/lib/mauve/notifiers/xmpp.rb
+++ b/lib/mauve/notifiers/xmpp.rb
@@ -85,15 +85,25 @@ module Mauve
@client = nil
end
+ # The logger instance
+ #
+ # @return [Log4r::Logger]
def logger
# Give the logger a sane name
@logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"")
end
+ # Sets the client's JID
+ #
+ # @param [String] jid The JID required.
+ # @return [Jabber::JID] The client JID.
def jid=(jid)
@jid = JID.new(jid)
end
-
+
+ # Connects to the XMPP server, and sets up the roster
+ #
+ # @return [Jabber::Client, NilClass] The connected client, or nil in the case of failure
def connect
logger.debug "Starting connection to #{@jid}"
@@ -156,13 +166,14 @@ module Mauve
@client = nil
end
- #
- # Kills the processor thread
- #
def stop
@client.stop
end
-
+
+ #
+ # Closes the XMPP connection, if possible. Sets @client to nil.
+ #
+ # @return [NilClass]
def close
@closing = true
if @client
@@ -177,27 +188,32 @@ module Mauve
@client = nil
end
+ # Determines if the client is ready.
+ #
+ # @return [Boolean]
def ready?
@client.is_a?(Jabber::Client) and @client.is_connected?
end
- #
- # Takes an alert and converts it into a message.
- #
- def convert_alert_to_message(alert)
- 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.
-
+ # @param [String] destination 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).
+ #
+ # @param [Mauve::Alert] alert This is turned into a pretty
+ # message and sent to the destination as a message, if +conditions+
+ # are met.
+ #
+ # @param [Array] all_alerts Currently ignored.
+ #
+ # @param [Hash] conditions Conditions that determine if an alert should be sent
+ #
+ # @option conditions [Array] :if_presence Checks whether the jid in question
+ # has a presence matching one or more of the choices - see
+ # Mauve::Notifiers::Xmpp::Default#check_jid_has_presence for options.
+ #
+ # @return [Boolean]
def send_alert(destination, alert, all_alerts, conditions = {})
destination_jid = JID.new(destination)
@@ -650,7 +666,6 @@ EOF
# :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]
diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb
index b15db3f..6b2ba33 100644
--- a/lib/mauve/people_list.rb
+++ b/lib/mauve/people_list.rb
@@ -4,14 +4,18 @@ require 'mauve/calendar_interface'
module Mauve
- # Stores a list of name.
+ # Stores a list of Mauve::Person
+ #
#
- # @author Yann Golanski
class PeopleList
attr_reader :label, :list
- # Default contrustor.
+ # Create a new list
+ #
+ # @param [String] label The name of the list
+ # @raise [ArgumentError] if the label is not a string
+ #
def initialize(label)
raise ArgumentError, "people_list label must be a string" unless label.is_a?(String)
@label = label
@@ -20,6 +24,10 @@ module Mauve
alias username label
+ # Append an Array or String to a list
+ #
+ # @param [Array or String] arr
+ # @return [Mauve::PeopleList] self
def +(arr)
case arr
when Array
@@ -44,15 +52,14 @@ module Mauve
alias add_to_list +
- #
- # Set up the logger
+ # @return Log4r::Logger
def logger
@logger ||= Log4r::Logger.new self.class.to_s
end
- #
# Return the array of people
#
+ # @return [Array]
def people
l = list.collect do |name|
diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb
index 0df31ee..acab8b7 100644
--- a/lib/mauve/person.rb
+++ b/lib/mauve/person.rb
@@ -6,7 +6,19 @@ module Mauve
class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low, :email, :xmpp, :sms)
attr_reader :notification_thresholds, :last_pop3_login, :suppressed
-
+
+ # Set up a new Person
+ #
+ # @param [Hash] args The options for setting up the person
+ # @option args [String] :username The person's username
+ # @option args [String] :password The SHA1 sum of the person's password
+ # @option args [String] :holiday_url The URL that can be checked by Mauve::CalendarInterface#is_user_on_holiday?
+ # @option args [Proc] :urgent The block to execute when an urgent-level notification is issued
+ # @option args [Proc] :normal The block to execute when an normal-level notification is issued
+ # @option args [Proc] :low The block to execute when an low-level notification is issued
+ # @option args [String] :email The person's email address
+ # @option args [String] :sms The person's mobile number
+ #
def initialize(*args)
@notification_thresholds = nil
@suppressed = false
@@ -17,13 +29,19 @@ module Mauve
super(*args)
end
+ # @return Log4r::Logger
def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end
+ # Determines if notifications to the user are currently suppressed
+ #
+ # @return [Boolean]
def suppressed? ; @suppressed ; end
+ # Works out if a notification should be suppressed. If no parameters are supplied, it will
#
- # This
- #
+ # @param [Time] Theoretical time of notification
+ # @param [Time] Current time.
+ # @return [Boolean] If suppression is needed.
def should_suppress?(with_notification_at = nil, now = Time.now)
return self.notification_thresholds.any? do |period, previous_alert_times|
@@ -42,10 +60,10 @@ module Mauve
end
end
+ # The notification thresholds for this user
+ #
+ # @return [Hash]
def notification_thresholds
- #
- # By default send 10 thresholds in a minute maximum
- #
@notification_thresholds ||= { }
end
@@ -54,6 +72,13 @@ module Mauve
#
class NotificationCaller
+ # Set up the notification caller
+ #
+ # @param [Mauve::Person] person
+ # @param [Mauve::Alert] alert
+ # @param [Array] other_alerts
+ # @param [Hash] base_conditions
+ #
def initialize(person, alert, other_alerts, base_conditions={})
@person = person
@alert = alert
@@ -61,40 +86,52 @@ module Mauve
@base_conditions = base_conditions
end
+ # @return Log4r::Logger
def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end
+ # This method makes sure things like +xmpp+ and +email+ work.
#
- # This method makes sure things like
- #
- # xmpp
- #
- # works
+ # @param [String] name The notification method to use
+ # @param [Array or Hash] args Extra conditions to pass to this notification method
#
+ # @return [Boolean] if the notifcation has been successful
def method_missing(name, *args)
#
+ # Work out the notification method
+ #
+ notification_method = Configuration.current.notification_methods[name.to_s]
+
+ @logger.warn "Notification method '#{name}' not defined (#{@person.username})" if notification_method.nil?
+
+ #
# Work out the destination
#
if args.first.is_a?(String)
destination = args.pop
- else
+ elsif @person.respond_to?(name)
destination = @person.__send__(name)
+ else
+ destination = nil
end
+ @logger.warn "#{name} destination for #{@person.username} not set" if destination.nil?
+
if args.first.is_a?(Array)
conditions = @base_conditions.merge(args[0])
else
conditions = @base_conditions
end
- notification_method = Configuration.current.notification_methods[name.to_s]
-
- raise NoMethodError.new("#{name} not defined as a notification method") unless notification_method
- # 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.
- #
- res = notification_method.send_alert(destination, @alert, @other_alerts, conditions)
+ if notification_method and destination
+ # 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.
+ #
+ res = notification_method.send_alert(destination, @alert, @other_alerts, conditions)
+ else
+ res = false
+ end
#
# Log the result
@@ -108,10 +145,12 @@ module Mauve
end
- #
- #
# Sends the alert
#
+ # @param [Symbol] level Level at which the alert should be sent
+ # @param [Mauve::Alert] alert Alert we're notifiying about
+ #
+ # @return [Boolean] if the notification was successful
def send_alert(level, alert)
now = Time.now
@@ -132,8 +171,6 @@ module Mauve
return true
end
- # FIXME current_alerts is very slow. So much so it slows everything
- # down. A lot.
result = NotificationCaller.new(
self,
alert,
@@ -143,7 +180,7 @@ module Mauve
:was_suppressed => was_suppressed, }
).instance_eval(&__send__(level))
- if result
+ if [result].flatten.any?
#
# Remember that we've sent an alert
#
@@ -168,6 +205,7 @@ module Mauve
# This is currently very CPU intensive, and slows things down a lot. So
# I've commented it out when sending notifications.
#
+ # @return [Array] alerts relevant to this person
def current_alerts
Alert.all_raised.select do |alert|
my_last_update = AlertChanged.first(:person => username, :alert_id => alert.id)
diff --git a/lib/mauve/pop3_server.rb b/lib/mauve/pop3_server.rb
index 645fe0d..52f31d0 100644
--- a/lib/mauve/pop3_server.rb
+++ b/lib/mauve/pop3_server.rb
@@ -4,25 +4,41 @@ require 'digest/sha1'
module Mauve
#
- # API to control the web server
+ # The POP3 server, where messages can also be read.
#
class Pop3Server < MauveThread
include Singleton
attr_reader :port, :ip
-
+
+ # Initialize the server
+ #
+ # Default port is 1110
+ # Default IP is 0.0.0.0
+ #
def initialize
super
self.port = 1110
self.ip = "0.0.0.0"
end
+ #
+ # Set the port
+ #
+ # @param [Integer] pr
+ # @raise [ArgumentError] if the port is not sane
+ # ~
def port=(pr)
raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
@port = pr
end
+ #
+ # Set the IP address. Unfortunately IPv6 is not OK.
+ #
+ # @param [String] i The IP address required.
+ #
def ip=(i)
raise ArgumentError, "ip must be a string" unless i.is_a?(String)
#
@@ -32,22 +48,15 @@ module Mauve
@ip = i
end
-
+
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
- def main_loop
- unless @server and @server.running?
- @server = Mauve::Pop3Backend.new(@ip.to_s, @port)
- logger.info "Listening on #{@server.to_s}"
- #
- # The next statment doesn't return.
- #
- @server.start
- end
- end
-
+ #
+ # This stops the server
+ #
def stop
if @server.running?
@server.stop
@@ -58,25 +67,50 @@ module Mauve
super
end
+ #
+ # This stops the server faster than stop
+ #
def join
@server.stop! if @server
super
end
+ private
+
+ #
+ # This tarts the server, and keeps it going.
+ #
+ def main_loop
+ unless @server and @server.running?
+ @server = Mauve::Pop3Backend.new(@ip.to_s, @port)
+ logger.info "Listening on #{@server.to_s}"
+ #
+ # The next statment doesn't return.
+ #
+ @server.start
+ end
+ end
+
end
+ #
+ # This is the Pop3 Server itself. It is based on the Thin HTTP server, and hence EventMachine.
+ #
class Pop3Backend < Thin::Backends::TcpServer
+ #
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
- # Connect the server
+ # Initialize a new connection to the server
def connect
@signature = EventMachine.start_server(@host, @port, Pop3Connection)
end
+ # Disconnect the server, but only if EventMachine is still going.
def disconnect
#
# Only do this if EventMachine is still going.. The http_server may have
@@ -87,17 +121,25 @@ module Mauve
end
-
+ #
+ # This class represents and individual connection, and understands some POP3
+ # commands.
+ #
class Pop3Connection < EventMachine::Connection
+ # The username
attr_reader :user
+ # Default CR+LF combo.
CRLF = "\r\n"
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
+ # This is called once the connection has been established. It says hello
+ # to the client, and resets the state.
def post_init
logger.info "New connection"
send_data "+OK #{self.class.to_s} started"
@@ -107,6 +149,11 @@ module Mauve
@level = nil
end
+ # This returns a list of commands allowed in a state.
+ #
+ # @param [Symbol] The state to query, defaults to the current state.
+ # @return [Array] An array of permitted comands.
+ #
def permitted_commands(state=@state)
case @state
when :authorization
@@ -118,6 +165,10 @@ module Mauve
end
end
+ # This returns a list of capabilities in a given state.
+ #
+ # @param [Symbol] The state to query, defaults to the current state.
+ # @return [Array] An array of capabilities.
def capabilities(state=@state)
case @state
when :transaction
@@ -129,6 +180,27 @@ module Mauve
end
end
+ # This method handles a command, and parses it.
+ #
+ # The following POP3 commands are understood:
+ # QUIT
+ # USER
+ # PASS
+ # STAT
+ # LIST
+ # RETR
+ # DELE
+ # NOOP
+ # RSET
+ # CAPA
+ # UIDL
+ #
+ # The command is checked against a list of permitted commands, given the
+ # state of the connection, and returns an error if the command is
+ # forbidden.
+ #
+ # @param [String] data The data to process.
+ #
def receive_data (data)
data.split(CRLF).each do |cmd|
break if error?
@@ -165,16 +237,39 @@ module Mauve
end
end
end
-
+
+ # This sends the data back to the user. A CR+LF is joined to the end of
+ # the data.
+ #
+ # @param [String] d The data to send back.
def send_data(d)
d += CRLF
super unless error?
end
+ private
+
+ # This deals with CAPA, returning a string of capabilities in the current
+ # connection state.
+ #
+ # @param [String] a The complete CAPA command sent by the client.
+ #
def do_process_capa(a)
send_data (["+OK Capabilities follow:"] + self.capabilities + ["."]).join(CRLF)
end
+ # This deals with the USER command.
+ #
+ # Any of low, normal, urgent can be appended to the username, to select
+ # only alarms of that level to be shown.
+ #
+ # e.g.
+ # patrick+low
+ #
+ # will show only alerts of a LOW level.
+ #
+ # @param [String] s The complete USER command sent by the client.
+ #
def do_process_user(s)
allowed_levels = Mauve::AlertGroup::LEVELS.collect{|l| l.to_s}
@@ -195,6 +290,10 @@ module Mauve
end
end
+ # This processes the PASS command. It uses the Mauve::Authenticate class
+ # to authenticate the user. Once authenticated, the state is set to :transaction.
+ #
+ # @param [String] s The complete PASS command sent by the client.
def do_process_pass(s)
if @user and s =~ /\APASS +(\S+)/
@@ -209,16 +308,27 @@ module Mauve
end
end
+ #
+ # This just sends an "ERR Unknown command" string back to the user.
+ #
+ # @param [String] a The complete command from the client that caused this error.
def do_process_error(a)
send_data "-ERR Unknown comand."
end
+ # This does a NOOP.
+ #
+ # @param [String] a The complete NOOP command from the client.
def do_process_noop(a)
send_data "+OK Thanks."
end
+ # Delete is processed as a NOOP
alias do_process_dele do_process_noop
+ # This logs a user out, and closes the connection. The state is set to :update.
+ #
+ # @param [String] a The complete QUIT command from the client.
def do_process_quit(a)
@state = :update
@@ -227,10 +337,17 @@ module Mauve
close_connection_after_writing
end
+ # This sends the number of messages, and their size back to the client.
+ #
+ # @param [String] a The complete STAT command from the client.
def do_process_stat(a)
send_data "+OK #{self.messages.length} #{self.messages.inject(0){|s,m| s+= m[1].length}}"
end
+ # This sends a list of the messages back to the client.
+ #
+ # @param [String] a The complete LIST command from the client.
+ #
def do_process_list(a)
d = []
if a =~ /\ALIST +(\d+)\b/
@@ -249,6 +366,9 @@ module Mauve
send_data d.join(CRLF)
end
+ # This sends the UID of a message back to the client.
+ #
+ # @param [String] a The complete UIDL command from the client.
def do_process_uidl(a)
if a =~ /\AUIDL +(\d+)\b/
ind = $1.to_i
@@ -267,6 +387,10 @@ module Mauve
end
end
+ # This retrieves a message for the client.
+ #
+ # @param [String] a The complete RETR command from the client.
+ #
def do_process_retr(a)
if a =~ /\ARETR +(\d+)\b/
ind = $1.to_i
@@ -287,7 +411,11 @@ module Mauve
end
#
- # These are the messages in the mailbox.
+ # These are the messages in the mailbox. It looks for the first 100 alert_changed, and formats them into emails, and returns an array of
+ #
+ # [alert_changed, email]
+ #
+ # @return [Array] Array of alert_changeds and emails.
#
def messages
if @messages.empty?
diff --git a/lib/mauve/processor.rb b/lib/mauve/processor.rb
index d68c551..c8a1dfb 100644
--- a/lib/mauve/processor.rb
+++ b/lib/mauve/processor.rb
@@ -4,12 +4,23 @@ require 'mauve/mauve_thread'
module Mauve
+ #
+ # This class is a singlton thread which pops updates off the
+ # Server#packet_buffer and processes them as alert updates.
+ #
+ # It is responsible for de-bouncing updates, i.e. ones with duplicate
+ # transmission IDs.
+ #
class Processor < MauveThread
include Singleton
+ # This is the time after which transmission IDs are expired.
+ #
attr_reader :transmission_cache_expire_time
+ # Initialize the processor
+ #
def initialize
super
#
@@ -20,15 +31,58 @@ module Mauve
@transmission_cache_checked_at = Time.now
end
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
+ # Set the expiry time
+ #
+ # @param [Integer] i The number of seconds after which transmission IDs are considered unseen.
+ # @raise [ArgumentError] If +i+ is not an Integer
def transmission_cache_expire_time=(i)
raise ArgumentError, "transmission_cache_expire_time must be an integer" unless i.is_a?(Integer)
@transmission_cache_expire_time = i
end
+ # This expries the transmission cache
+ #
+ #
+ def expire_transmission_id_cache
+ now = Time.now
+ #
+ # Only check once every minute.
+ #
+ return unless (now - @transmission_cache_checked_at) > 60
+
+ 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
+
+ @transmission_cache_checked_at = now
+ end
+
+ # This stops the processor, making sure all pending updates are saved.
+ #
+ def stop
+ super
+
+ #
+ # flush the queue
+ #
+ main_loop
+ end
+
+ private
+
+ # This is the main loop that does the processing.
+ #
def main_loop
sz = Server.packet_buffer_size
@@ -92,33 +146,7 @@ module Mauve
Timer.instance.thaw if Timer.instance.frozen?
end
- def expire_transmission_id_cache
- now = Time.now
- #
- # Only check once every minute.
- #
- return unless (now - @transmission_cache_checked_at) > 60
-
- 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
-
- @transmission_cache_checked_at = now
- end
-
- def stop
- super
-
- #
- # flush the queue
- #
- main_loop
- end
end
+
end
+
diff --git a/lib/mauve/sender.rb b/lib/mauve/sender.rb
index 6685de5..c84dcb0 100644
--- a/lib/mauve/sender.rb
+++ b/lib/mauve/sender.rb
@@ -4,12 +4,40 @@ require 'socket'
require 'mauve/mauve_resolv'
require 'mauve/mauve_time'
-module Mauve
+module Mauve
+ #
+ # This is the class that send the Mauve packets.
+ #
class Sender
+ #
+ # This is the default mauve receiving port.
+ #
DEFAULT_PORT = 32741
include Resolv::DNS::Resource::IN
-
+
+ # Set up a new sender. It takes a list of destinations and uses DNS to
+ # resolve names to addresses.
+ #
+ # A destination can look like
+ #
+ # 1.2.3.4:5678
+ # 1.2.3.4
+ # [2001:1af:ba8::dead]:5678
+ # [2001:1af:ba8::dead]
+ # mauve.host:5678
+ # mauve.host
+ #
+ # If no port is specified, DEFAULT_PORT is used.
+ #
+ # If a hostname is used, SRV records are used, with the prefix
+ # +_mauvealert._udp+ to determine the real hostname and port to which
+ # alerts should be sent.
+ #
+ # Otherwise AAAA and A records are looked up.
+ #
+ # @param [Array] destinations List of destinations to which the update is to be sent.
+ #
def initialize(*destinations)
destinations = destinations.flatten
@@ -101,9 +129,12 @@ module Mauve
end
+ # Send an update.
#
- # Returns the number of packets sent.
+ # @param [Mauve::Proto] update The update to send
+ # @param [Integer] vebose The verbosity -- higher is more.
#
+ # @return [Integer] the number of packets sent.
def send(update, verbose=0)
#
diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb
index a217aab..41a76dc 100644
--- a/lib/mauve/server.rb
+++ b/lib/mauve/server.rb
@@ -25,11 +25,14 @@ module Mauve
#
THREAD_CLASSES = [UDPServer, HTTPServer, Pop3Server, Processor, Timer, Notifier, Heartbeat]
- attr_reader :hostname, :database, :initial_sleep
+ attr_reader :hostname, :database, :initial_sleep
attr_reader :packet_buffer, :notification_buffer, :started_at
include Singleton
+ # Initialize the Server, setting up a blank configuration if no
+ # configuration has been created already.
+ #
def initialize
super
@hostname = "localhost"
@@ -51,29 +54,47 @@ module Mauve
Configuration.current = Configuration.new if Mauve::Configuration.current.nil?
end
+ # Set the hostname of this Mauve instance.
+ #
+ # @param [String] h The hostname
def hostname=(h)
raise ArgumentError, "hostname must be a string" unless h.is_a?(String)
@hostname = h
end
+ # Set the database
+ #
+ # @param [String] d The database
def database=(d)
raise ArgumentError, "database must be a string" unless d.is_a?(String)
@database = d
end
-
+
+ # Set the sleep period during which notifications about old alerts are
+ # suppressed.
+ #
+ # @param [Integer] s The initial sleep period.
def initial_sleep=(s)
raise ArgumentError, "initial_sleep must be numeric" unless s.is_a?(Numeric)
@initial_sleep = s
end
+ # Test to see if we should suppress alerts because we're in the initial sleep period
+ #
+ # @return [Boolean]
def in_initial_sleep?
Time.now < self.started_at + self.initial_sleep
end
+ # return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
+ # This sorts out the Server. It empties the notification and packet
+ # buffers. It configures and migrates the database.
+ #
+ # @return [NilClass]
def setup
#
#
@@ -103,15 +124,45 @@ module Mauve
return nil
end
+ # This sets up the server, and then starts the main loop.
+ #
def start
# self.state = :starting
- self.setup
+ setup
- self.run_thread { self.main_loop }
+ run_thread { main_loop }
end
alias run start
+
+ #
+ # This stops the main loop, and all the threads that are defined in THREAD_CLASSES above.
+ #
+ def stop
+ if self.state == :stopping
+ # uh-oh already told to stop.
+ logger.error "Stop already called. Killing self!"
+ Kernel.exit 1
+ end
+
+ self.state = :stopping
+
+ THREAD_CLASSES.each do |klass|
+ klass.instance.stop unless klass.instance.nil?
+ end
+
+ thread_list = Thread.list
+ thread_list.delete(Thread.current)
+
+ thread_list.each do |t|
+ t.exit
+ end
+
+ self.state = :stopped
+ end
+
+ private
def main_loop
thread_list = Thread.list
@@ -185,29 +236,6 @@ module Mauve
end
end
- def stop
- if self.state == :stopping
- # uh-oh already told to stop.
- logger.error "Stop already called. Killing self!"
- Kernel.exit 1
- end
-
- self.state = :stopping
-
- THREAD_CLASSES.each do |klass|
- klass.instance.stop unless klass.instance.nil?
- end
-
- thread_list = Thread.list
- thread_list.delete(Thread.current)
-
- thread_list.each do |t|
- t.exit
- end
-
- self.state = :stopped
- end
-
class << self
@@ -218,17 +246,23 @@ module Mauve
# processing them crash, the buffer itself is not lost with the thread.
#
- #
- # These methods pop things on and off the packet_buffer
- #
+ # Push a packet onto the back of the +packet_buffer+
+ #
+ # @param [String] a Packet from the UDP server
def packet_enq(a)
instance.packet_buffer.push(a)
end
+ # Shift a packet off the front of the +packet buffer+
+ #
+ # @return [String] the oldest UDP packet
def packet_deq
instance.packet_buffer.shift
end
+ # Returns the current length of the +packet_buffer+
+ #
+ # @return [Integer}
def packet_buffer_size
instance.packet_buffer.size
end
@@ -236,17 +270,23 @@ module Mauve
alias packet_push packet_enq
alias packet_pop packet_deq
+ # Push a notification on to the back of the +notification_buffer+
#
- # These methods pop things on and off the notification_buffer
- #
+ # @param [Array] a Notification array, consisting of a Person and the args to Mauve::Person#send_alert
def notification_enq(a)
instance.notification_buffer.push(a)
end
+ # Shift a notification off the front of the +notification_buffer+
+ #
+ # @return [Array] Notification array, consisting of a Person and the args to Mauve::Person#send_alert
def notification_deq
instance.notification_buffer.shift
end
+ # Return the current length of the +notification_buffer+
+ #
+ # @return [Integer]
def notification_buffer_size
instance.notification_buffer.size
end
diff --git a/lib/mauve/source_list.rb b/lib/mauve/source_list.rb
index caa9f9a..b8c89cc 100644
--- a/lib/mauve/source_list.rb
+++ b/lib/mauve/source_list.rb
@@ -9,20 +9,14 @@ 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.
+ # converted into one as 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.
+ # will occur.
#
- # @author Yann Golanski
class SourceList
attr_reader :label, :list
@@ -37,6 +31,26 @@ module Mauve
alias username label
+ # Adds a source onto the list.
+ #
+ # The source can be a string, or array of strings. Each one can be an IPv6
+ # or IPv4 address or range, or a hostname.
+ #
+ # Hostnames can have *, or numeric ranges in their name. A '*' represents
+ # any character except ".". A range can be specified as 1..4, meaning 1,
+ # 2, 3 or 4.
+ #
+ # e.g. 1.2.3.4/24
+ # 2001:dead::beef/64
+ # app1..10.my-customer.com
+ # *.db.my-customer.com
+ #
+ # Hostnames are also resolved into IP addresses, and re-resolved every 30
+ # minutes.
+ #
+ # @param [String or Array] l The source(s) to add.
+ # @return [SourceList]
+ #
def +(l)
arr = [l].flatten.collect do |h|
# "*" means [^\.]+
@@ -52,12 +66,15 @@ module Mauve
gsub(/\./, "\\.").
gsub(/\*/, "[0-9a-z\\-]+") +
"\\.?$")
- elsif h.is_a?(String) and h =~ /^[0-9a-f\.:\/]+$/i
+ elsif h.is_a?(String) and h =~ /^[0-9a-f\.:]+(\/\d+)?$/i
IPAddr.new(h)
- else
+ elsif h.is_a?(String) or h.is_a?(Regexp)
h
+ else
+ logger.warn "Cannot add #{h.inspect} to source list #{@label} as it is not a string or regular expression."
+ nil
end
- end.flatten
+ end.flatten.reject{|h| h.nil?}
arr.each do |source|
##
@@ -79,13 +96,26 @@ module Mauve
alias add_to_list +
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new self.class.to_s
end
- ##
+ #
# Return whether or not a list contains a source.
- ##
+ #
+ # First the hostname is checked for a URI, using URI#parse, and then the
+ # hostname is extracted from there. If that fails, the original hostname
+ # is used.
+ #
+ # Next we check against our list, including all IPs for any hostnames in
+ # that list.
+ #
+ # If nothing is found, the hostname is then resolved to its IPs, and we
+ # check to see if those IPs are in our list.
+ #
+ # @param [String] host The host to look for.
+ # @return [Boolean]
def includes?(host)
#
# Redo resolution every thirty minutes
@@ -133,14 +163,20 @@ module Mauve
ips.any?{|ip| list_ip.include?(ip)}
end
+ return false
end
+ #
+ # Resolve all hostnames in the list to IP addresses.
+ #
+ # @return [Array] The new list.
+ #
def resolve
@last_resolved_at = Time.now
new_list = @list.collect do |host|
if host.is_a?(String)
- ips = [host] + MauveResolv.get_ips_for(host).collect{|i| IPAddr.new(i)}
+ [host] + MauveResolv.get_ips_for(host).collect{|i| IPAddr.new(i)}
else
host
end
diff --git a/lib/mauve/timer.rb b/lib/mauve/timer.rb
index a00d66d..5a43b60 100644
--- a/lib/mauve/timer.rb
+++ b/lib/mauve/timer.rb
@@ -7,6 +7,9 @@ require 'log4r'
module Mauve
+ #
+ # This is the thread that looks for reminders and heartbeat alerts to poll.
+ #
class Timer < MauveThread
include Singleton
@@ -20,6 +23,14 @@ module Mauve
super
end
+ private
+
+ # This is the trigger for heartbeats and reminders.
+ #
+ # It looks up the next event, and sleeps until it is due. If an update
+ # comes in (via the processor) it is broken out of its sleep, and starts
+ # again when woken up.
+ #
def main_loop
#
# Get the next alert.
diff --git a/lib/mauve/udp_server.rb b/lib/mauve/udp_server.rb
index 080a04b..e3362e8 100644
--- a/lib/mauve/udp_server.rb
+++ b/lib/mauve/udp_server.rb
@@ -8,12 +8,22 @@ require 'ipaddr'
module Mauve
+ #
+ # This is the thread that accepts the packets.
+ #
class UDPServer < MauveThread
include Singleton
attr_reader :ip, :port
+ # Yup. Creates a new UDPServer.
+ #
+ # Defaults:
+ # * listening IP: 127.0.0.1
+ # * listening port: 32741
+ # * polls every: 0 seconds
+ #
def initialize
#
# Set up some defaults.
@@ -26,16 +36,50 @@ module Mauve
super
end
+ #
+ # This sets the IP which the server will listen on.
+ #
+ # @param [String] i The new IP
+ # @return [IPAddr]
+ #
def ip=(i)
raise ArgumentError, "ip must be a string" unless i.is_a?(String)
@ip = IPAddr.new(i)
end
+ # Sets the listening port
+ #
+ # @param [Integer] pr The new port
+ # @return [Integer]
+ #
def port=(pr)
raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
@port = pr
end
+
+ # This stops the UDP server, by signalling to the thread to stop, and
+ # sending a zero-length packet to the socket.
+ #
+ def stop
+ @stop = true
+ #
+ # Triggers loop to close socket.
+ #
+ UDPSocket.open(Socket.const_get(@socket.addr[0])).send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.nil? or @socket.closed?
+
+ super
+ end
+
+ private
+ #
+ # This opens the socket for listening.
+ #
+ # It tries to increase the default receiving buffer to 10M, and will warn
+ # if this fails to increase the buffer at all.
+ #
+ # @return [Nilclass]
+ #
def open_socket
#
# Specify the family when opening the socket.
@@ -48,7 +92,7 @@ module Mauve
@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.warn "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}.")
@@ -57,6 +101,8 @@ module Mauve
logger.info("Opened socket on #{@ip.to_s}:#{@port}")
end
+ # This closes the socket. IOErrors are caught and logged.
+ #
def close_socket
return if @socket.nil? or @socket.closed?
@@ -71,6 +117,13 @@ module Mauve
logger.info("Closed socket")
end
+ # This is the main loop. It receives from the UDP port, and records the
+ # time the packet arrives. It then pushes the packet onto the Server's
+ # packet buffer for the processor to pick up later.
+ #
+ # If a zero-length packet is received, and the thread has been signalled to
+ # stop, then the socket is closed, and the thread exits.
+ #
def main_loop
return if self.should_stop?
@@ -99,7 +152,7 @@ module Mauve
# 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
+ close_socket
return
end
@@ -109,16 +162,6 @@ module Mauve
Server.packet_push([packet[0], packet[1], received_at])
end
- def stop
- @stop = true
- #
- # Triggers loop to close socket.
- #
- UDPSocket.open(Socket.const_get(@socket.addr[0])).send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.nil? or @socket.closed?
-
- super
- end
-
end
end
diff --git a/lib/mauve/version.rb b/lib/mauve/version.rb
index 56ac522..2b35608 100644
--- a/lib/mauve/version.rb
+++ b/lib/mauve/version.rb
@@ -1,5 +1,10 @@
+#
+# This is Mauve!
+#
module Mauve
+ #
+ # Current version
VERSION="3.5.8"
end