diff options
author | Patrick J Cherry <patrick@bytemark.co.uk> | 2012-04-25 17:15:49 +0100 |
---|---|---|
committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2012-04-25 17:15:49 +0100 |
commit | e959c0fe4c887154bbe28c31324fef2975cbe467 (patch) | |
tree | 3088c7a1f389944d613e57b551b452f7ec83181d | |
parent | 5fff12fc11cb8b02a44fd40ed78fa9d196f269d7 (diff) |
Big update.
* Max acknowledgement time is now specified in the config
* Calendar interface improved.
* holiday_url no longer used -- replaced by notify_when_on_holiday!
* added notify_when_off_sick!
* Added ability for the calendar to be queried for a list of bank holdays.
* Added ability for Time to be given a list of bank holidays to check against.
* PeopleLists can now be a Proc, allowing downloading of lists
* Person is no longer a struct
* Moved the method_missing bit into ObjectBuilder from various sub classes.
* Added tests for the calendar interface
* Updated tests in other bits.
26 files changed, 610 insertions, 273 deletions
diff --git a/bin/mauveserver b/bin/mauveserver index b0c3573..cc2b124 100644 --- a/bin/mauveserver +++ b/bin/mauveserver @@ -34,6 +34,8 @@ # Patrick J Cherry <patrick@bytemark.co.uk> # +$:.unshift File.expand_path(File.join(File.dirname(__FILE__),"..","lib")) + help = manual = verbose = version = test = false while arg = ARGV.pop case arg diff --git a/example.conf b/example.conf index 4123d8e..347c33d 100644 --- a/example.conf +++ b/example.conf @@ -5,13 +5,18 @@ server { # # This is where our database lives. SQLite is the default. # - database "sqlite3::memory:" + + # database "sqlite3::memory:" + database "postgres://localhost/mauvealert" # # This is our hostname. It gets used when URLs are generated, and in the heartbeat alert. # hostname "mauve.example.com" +# use_packet_buffer "no" +# use_notification_buffer "no" + # # This is the UDP listener. # @@ -27,7 +32,7 @@ server { # # This is how long the UDP server will sleep between looking for packets. # - poll_every 1 + poll_every 0 } @@ -40,7 +45,7 @@ server { # This is the length of time the processor will sleep between checking for # new packets from the UDP listener. # - poll_every 1 + poll_every 0 # # In order to make sure the same transmission isn't received more then @@ -78,23 +83,23 @@ server { # This is where the mauve server sends its own heartbeat. Useful for # watching the watcher. # - heartbeat { +# heartbeat { # # If no destination is specified, then the contents of # /etc/mauvealert/mauvesend.destination are used. # - destination "localhost" +# destination "localhost" # # This is how long to wait before the alert is raised # - raise_after 600 +# raise_after 600 # # These two fields have sensible defaults set, but more informative # messages can be set here. # - summary "Mauve alert server is down" - detail "The Mauve alert server has failed to send a heartbeat" - } +# summary "Mauve alert server is down" +# detail "The Mauve alert server has failed to send a heartbeat" +# } } @@ -137,7 +142,7 @@ logger { outputter("file") { filename "/tmp/mauveserver.log" trunc true - level Log4r::DEBUG + level Log4r::INFO } # outputter("email") { @@ -154,24 +159,16 @@ logger { # XMPP instant messaging. This are the credentials to log into the XMPP # account that mauve will use. # -#notification_method ("xmpp") { -# jid "mauve@chat.example.com/mauve" -# password "mauvespassword" -#} - -# -# Email messaging. -# -notification_method ("email") { - server "localhost" - from "mauve@desk1.tur.bytemark.co.uk" - subject_prefix "mauve-test" +# XMPP instant messaging. +notification_method ("xmpp") { + jid "bytemark@chat.bytemark.co.uk/pjc-test" + password "OsPzCmqSb" } person("office_chat") { -# xmpp "muc:mauve-test@conference.chat.bytemark.co.uk/MauveAlert" -# all { xmpp } -# suppress_notifications_after(310 => 1.minute) + xmpp "muc:mauve-test@conference.chat.bytemark.co.uk/MauveAlert" + all { xmpp } + suppress_notifications_after(310 => 1.minute) } person ("pcherry") { @@ -179,17 +176,13 @@ person ("pcherry") { all { true } } -people_list ("arse") { - list [ "office_chat" ] -} - # # Default notification - tell root about all alerts every hour # alert_group("default") { level URGENT - notify("arse") { + notify("office_chat") { every 2.minutes } } diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb index f249913..6b08f82 100644 --- a/lib/mauve/alert.rb +++ b/lib/mauve/alert.rb @@ -420,7 +420,7 @@ module Mauve # # Limit acknowledgment time. # - limit = Time.now + 15.days + limit = Time.now + Configuration.current.max_acknowledgement_time ack_until = limit if ack_until > limit self.acknowledged_by = person.username diff --git a/lib/mauve/authentication.rb b/lib/mauve/authentication.rb index c467a1d..24c6bd3 100644 --- a/lib/mauve/authentication.rb +++ b/lib/mauve/authentication.rb @@ -70,7 +70,7 @@ module Mauve unless true == result logger.info "Authentication for #{login} failed" # Rate limit - sleep Server.instance.failed_login_delay + sleep Configuration.current.failed_login_delay end result @@ -97,15 +97,15 @@ module Mauve # # Don't bother checking if no auth_url has been set. # - return false unless Server.instance.bytemark_auth_url.is_a?(URI) + return false unless Configuration.current.bytemark_auth_url.is_a?(URI) # # Don't bother checking if the person doesn't exist. # return false unless Mauve::Configuration.current.people.has_key?(login) - uri = Server.instance.bytemark_auth_url - timeout = Server.instance.remote_http_timeout + uri = Configuration.current.bytemark_auth_url + timeout = Configuration.current.remote_http_timeout # host=nil, path=nil, port=nil, proxy_host=nil, proxy_port=nil, user=nil, password=nil, use_ssl=nil, timeout=nil) client = XMLRPC::Client.new(uri.host, uri.path, uri.port, nil, nil, uri.user, uri.password, uri.scheme == "https", timeout) @@ -114,7 +114,7 @@ module Mauve # if client.http.use_ssl? client.http.ca_path = "/etc/ssl/certs/" - client.http.verify_mode = Server.instance.remote_https_verify_mode + client.http.verify_mode = Configuration.current.remote_https_verify_mode end begin diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb index cf515d1..692981d 100644 --- a/lib/mauve/calendar_interface.rb +++ b/lib/mauve/calendar_interface.rb @@ -17,52 +17,58 @@ module Mauve @logger ||= Log4r::Logger.new(self.to_s) end - # Gets a list of ssologin on support. + def get_attendees(klass, at=Time.now) + # + # Returns nil if no calendar_url has been set. + # + return [] unless Configuration.current.bytemark_calendar_url + + url = Configuration.current.bytemark_calendar_url.dup + + url.merge!(File.join(url.path, "/api/attendees/#{klass}/#{at.strftime("%Y-%m-%dT%H:%M:00")}")) + ans = do_get(url) + + return [] unless ans.is_a?(Array) + + ans.select{|x| x.is_a?(String)} + end + # - # @param [String] url A Calendar API url. + # This should return a list of dates of forthcoming bank holidays # - # @return [Array] A list of all the usernames on support. - def get_users_on_support(url) - result = do_get_with_cache(url) - - if result.is_a?(String) - result = result.split("\n") - else - result = [] - end + def get_bank_holiday_list(at = Time.now) + return [] unless Configuration.current.bytemark_calendar_url - return result + url = Configuration.current.bytemark_calendar_url.dup + url.merge!(File.join(url.path, "/api/bank_holidays/#{at.strftime("%Y-%m-%d")}")) + ans = do_get(url) + + return [] unless ans.is_a?(Array) + ans.select{|x| x.is_a?(Date)} end - # Check to see if the user is on support. + # Check to see if the user is on holiday. # - # @param [String] url A Calendar API url. - # @param [String] usr User single sign on. + # Class method. # - # @return [Boolean] True if on support, false otherwise. - def is_user_on_support?(url, usr) - return get_users_on_support(url).include?(usr) + # @param [String] usr User single sign on. + # + # @return [Boolean] True if on holiday, false otherwise. + def is_user_on_holiday?(usr, at=Time.now) + get_attendees("staff_holiday").include?(usr) end # Check to see if the user is on holiday. # # Class method. # - # @param [String] url A Calendar API url. # @param [String] usr User single sign on. # # @return [Boolean] True if on holiday, false otherwise. - def is_user_on_holiday?(url) - result = do_get_with_cache(url) - - if result.is_a?(String) and result =~ /^\d{4}(-\d\d){2}[ T](\d\d:){2}\d\d/ - return true - else - return false - end + def is_user_off_sick?(usr, at=Time.now) + get_attendees("sick_period", at).include?(usr) end - private # Grab a URL from the wide web. @@ -74,7 +80,7 @@ module Mauve # def do_get (uri, limit = 11) - if 0 == limit + if 0 > limit logger.warn("HTTP redirect too deep for #{uri}.") return nil end @@ -99,8 +105,22 @@ module Mauve response = http.start { http.get(uri.request_uri()) } if response.is_a?(Net::HTTPOK) - return response.body + # + # Parse the string as YAML. + # + result = if response.body.is_a?(String) + begin + YAML.load(response.body) + rescue YAML::Error => err + logger.error "Caught #{ex.class.to_s} (#{ex.to_s}) whilst querying #{url.to_s}." + logger.debug err.backtrace.join("\n") + nil + end + else + nil + end + return result elsif response.is_a?(Net::HTTPRedirection) and response.key?('Location') location = response['Location'] @@ -123,6 +143,7 @@ module Mauve logger.error("Timeout caught during fetch of #{uri.to_s}.") rescue StandardError => ex + pp ex.backtrace logger.error("#{ex.class} caught during fetch of #{uri.to_s}: #{ex.to_s}.") logger.debug(ex.backtrace.join("\n")) @@ -140,10 +161,10 @@ module Mauve def do_get_with_cache(url, cache_until = Time.now + 5.minutes) @cache ||= {} - if @cache.has_key?(url.to_s) - result, cache_until = @cache[url.to_s] + if @cache.has_key?(url) + result, cached_until = @cache[url] - return result if cache_until >= Time.now and not result.nil? + return result if cached_until > Time.now and not result.nil? end result = do_get(url) @@ -152,6 +173,19 @@ module Mauve return result end + # + # This should get called periodically. + # + def clean_cache + + @cache.keys.select do |url| + result, cached_until = @cache[url] + @cache.delete(url) if !cached_until.is_a?(Time) or cached_until <= Time.now + end + + @cache + end + end end diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb index 65f5f71..3a8f9cc 100644 --- a/lib/mauve/configuration.rb +++ b/lib/mauve/configuration.rb @@ -40,6 +40,12 @@ module Mauve # @return [Hash] attr_reader :source_lists + # Various further configuration items + # + attr_reader :bytemark_auth_url, :bytemark_calendar_url, :remote_http_timeout, :remote_https_verify_mode, :failed_login_delay + attr_reader :max_acknowledgement_time + + # # Set up a base config. # @@ -50,7 +56,114 @@ module Mauve @people_lists = {} @source_lists = Hash.new{|h,k| h[k] = Mauve::SourceList.new(k)} @alert_groups = [] + + # + # Set the auth/calendar URLs + # + @bytemark_auth_url = nil + @bytemark_calendar_url = nil + + # + # Set a couple of params for remote HTTP requests. + # + @remote_http_timeout = 5 + @remote_https_verify_mode = OpenSSL::SSL::VERIFY_PEER + + # + # Rate limit login attempts to limit the success of brute-forcing. + # + @failed_login_delay = 1 + + # + # Maximum amount of time to acknowledge for + # + @max_acknowledgement_time = 15.days + end + + # Set the calendar URL. + # + # @param [String] arg + # @return [URI] + def bytemark_calendar_url=(arg) + raise ArgumentError, "bytemark_calendar_url must be a string" unless arg.is_a?(String) + + @bytemark_calendar_url = URI.parse(arg) + + # + # Make sure we get an HTTP URL. + # + raise ArgumentError, "bytemark_calendar_url must be an HTTP(S) URL." unless %w(http https).include?(@bytemark_calendar_url.scheme) + + # + # Set a default request path, if none was given + # + @bytemark_calendar_url.normalize! + + @bytemark_calendar_url + end + + # Set the Bytemark Authentication URL + # + # @param [String] arg + # @return [URI] + def bytemark_auth_url=(arg) + raise ArgumentError, "bytemark_auth_url must be a string" unless arg.is_a?(String) + + @bytemark_auth_url = URI.parse(arg) + # + # Make sure we get an HTTP URL. + # + raise ArgumentError, "bytemark_auth_url must be an HTTP(S) URL." unless %w(http https).include?(@bytemark_auth_url.scheme) + + # + # Set a default request path, if none was given + # + @bytemark_auth_url.normalize! + + @bytemark_auth_url + end + + # Sets the timeout when making remote HTTP requests + # + # @param [Integer] arg + # @return [Integer] + def remote_http_timeout=(arg) + raise ArgumentError, "remote_http_timeout must be an integer" unless s.is_a?(Integer) + @remote_http_timeout = arg + end + + # Sets the SSL verification mode when makeing remote HTTPS requests + # + # @param [String] arg must be one of "none" or "peer" + # @return [Constant] + def remote_https_verify_mode=(arg) + @remote_https_verify_mode = case arg + when "peer" + OpenSSL::SSL::VERIFY_PEER + when "none" + OpenSSL::SSL::VERIFY_NONE + else + raise ArgumentError, "remote_https_verify_mode must be either 'peer' or 'none'" + end + end + + # Set the delay added following a failed login attempt. + # + # @param [Numeric] arg Number of seconds to delay following a failed login attempt + # @return [Numeric] + # + def failed_login_delay=(arg) + raise ArgumentError, "failed_login_delay must be numeric" unless arg.is_a?(Numeric) + @failed_login_delay = arg end + # Set the maximum amount of time alerts can be ack'd for + # + # + def max_acknowledgement_time=(arg) + raise ArgumentError, "max_acknowledgement_time must be numeric" unless arg.is_a?(Numeric) + @max_acknowledgement_time = arg + end + end end diff --git a/lib/mauve/configuration_builder.rb b/lib/mauve/configuration_builder.rb index 3f7138e..c689c1b 100644 --- a/lib/mauve/configuration_builder.rb +++ b/lib/mauve/configuration_builder.rb @@ -29,18 +29,6 @@ module Mauve @result.source_lists[label] += list end - # Adds a people list - # - # @param [String] label - # @param [Array] list - # - # @return [Array] the whole people list for label - # ef 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] diff --git a/lib/mauve/configuration_builders/notification_method.rb b/lib/mauve/configuration_builders/notification_method.rb index 3f9283e..7951dee 100644 --- a/lib/mauve/configuration_builders/notification_method.rb +++ b/lib/mauve/configuration_builders/notification_method.rb @@ -40,18 +40,6 @@ module Mauve result.extend(Mauve::Notifiers::Debug) 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) - else - result.send(name.to_sym) - end - end end end diff --git a/lib/mauve/configuration_builders/person.rb b/lib/mauve/configuration_builders/person.rb index dff6f85..7a20491 100644 --- a/lib/mauve/configuration_builders/person.rb +++ b/lib/mauve/configuration_builders/person.rb @@ -18,12 +18,14 @@ module Mauve is_block_attribute "urgent" is_block_attribute "normal" is_block_attribute "low" + is_attribute "password" is_attribute "sms" - is_attribute "holiday_url" is_attribute "email" is_attribute "xmpp" - is_attribute "sms" + + is_flag_attribute "notify_when_on_holiday!" + is_flag_attribute "notify_when_off_sick!" # Sets the block for all levels of alert # diff --git a/lib/mauve/configuration_builders/server.rb b/lib/mauve/configuration_builders/server.rb index d22ed87..734fb3b 100644 --- a/lib/mauve/configuration_builders/server.rb +++ b/lib/mauve/configuration_builders/server.rb @@ -171,31 +171,6 @@ module Mauve is_attribute "use_packet_buffer" is_attribute "use_notification_buffer" - # - # This is where the calendar is located. The request paths are hard-coded. - # - is_attribute "bytemark_calendar_url" - - # - # This is where the Bytemark authentication server is located. - # - is_attribute "bytemark_auth_url" - - # - # This is the level of SSL verification used when making external HTTPS connections. - # - is_attribute "remote_https_verify_mode" - - # - # This is the default timeout when making remote HTTP requests - # - is_attribute "remote_http_timeout" - - # - # This is the default sleep time after an authentication attempt has failed. - # - is_attribute "failed_login_delay" - def builder_setup @result = Mauve::Server.instance end diff --git a/lib/mauve/mauve_time.rb b/lib/mauve/mauve_time.rb index f564fdb..dfbf99b 100644 --- a/lib/mauve/mauve_time.rb +++ b/lib/mauve/mauve_time.rb @@ -98,12 +98,37 @@ class Time t end + # Returns the bank_holidays array, or an empty array if bank_holidays hasn't + # been set. + # + # + def bank_holidays + @bank_holidays ||= [] + end + + # This sets the bank holiday dates for the bank_holiday? check. + # + # @param [Array] arg An array of Date of bank holidays + # @returns [Array] + # + def bank_holidays=(arg) + raise ArgumentError unless arg.is_a?(Array) and arg.all?{|a| a.is_a?(Date)} + @bank_holidays = arg + end + + # This relies on bank_holidays being set. + # + def bank_holiday? + today = Date.new(self.year, self.month, self.day) + self.bank_holidays.any?{|bh| bh == today} + end + # Test to see if we're in working hours. 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)) + !bank_holiday? and (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 @@ -113,7 +138,6 @@ class Time (8..21).include?(self.hour) end - # We're always in wallclock hours # # @return [true] diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb index d06e03a..5877e0f 100644 --- a/lib/mauve/notification.rb +++ b/lib/mauve/notification.rb @@ -163,8 +163,9 @@ module Mauve # Test to see if we're in working hours. See Time#working_hours? # # @return [Boolean] - def working_hours? + def working_hours? @test_time = @time if @test_time.nil? + @test_time.bank_holidays = Server.instance.bank_holidays @test_time.working_hours? end diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb index a0ad86e..b291442 100644 --- a/lib/mauve/notifiers/xmpp.rb +++ b/lib/mauve/notifiers/xmpp.rb @@ -604,7 +604,10 @@ EOF end begin - ack_until = Time.now.in_x_hours(n_hours, type_hours) + now = Time.now + now.bank_holidays = Server.instance.bank_holidays + + ack_until = now.in_x_hours(n_hours, type_hours) rescue RangeError return "I'm sorry, you tried to acknowedge for far too long, and my buffers overflowed!" end diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb index eab4ae3..0433034 100644 --- a/lib/mauve/people_list.rb +++ b/lib/mauve/people_list.rb @@ -35,6 +35,8 @@ module Mauve arr = arr.flatten when String arr = [arr] + when Proc + arr = [arr] else logger.warn "Not sure what to do with #{arr.inspect} -- converting to string, and continuing" arr = [arr.to_s] @@ -62,10 +64,12 @@ module Mauve # # @return [Array] def people - l = list.collect do |name| - Configuration.current.people.has_key?(name) ? Configuration.current.people[name] : nil - end.reject{|person| person.nil?} + name.is_a?(Proc) ? name.call : name + end.flatten.compact.uniq.collect do |name| + Configuration.current.people[name] + end.compact + # # Hmm.. no-one in the list?! # diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb index 1cf4ea0..929f511 100644 --- a/lib/mauve/person.rb +++ b/lib/mauve/person.rb @@ -3,23 +3,15 @@ require 'timeout' require 'log4r' module Mauve - class Person < Struct.new(:username, :password, :urgent, :normal, :low, :email, :xmpp, :sms) + class Person + attr_reader :username, :password, :urgent, :normal, :low, :email, :xmpp, :sms attr_reader :notification_thresholds, :last_pop3_login, :suppressed, :notifications - + attr_reader :notify_when_off_sick, :notify_when_on_holiday + # 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) + def initialize(username) @notification_thresholds = nil @suppressed = false # @@ -28,9 +20,85 @@ module Mauve @last_pop3_login = {:from => nil, :at => nil} @notifications = [] - super(*args) - end + @username = username + @password = nil + @urgent = lambda { false } + @normal = lambda { false } + @low = lambda { false } + @email = @sms = @xmpp = nil + @notify_when_on_holiday = @notify_when_off_sick = false + end + + # Determines if a user should be notified if they're ill. + # + # @return [Boolean] + # + def notify_when_off_sick! + @notify_when_off_sick = true + end + + # Determines if a user should be notified if they're on their holdiays. + # + # @return [Boolean] + # + def notify_when_on_holiday! + @notify_when_on_holiday = true + end + + # Sets the Proc to call for urgent notifications + # + def urgent=(block) + raise ArgumentError unless block.is_a?(Proc) + @urgent = block + end + + # Sets the Proc to call for normal notifications + # + def normal=(block) + raise ArgumentError unless block.is_a?(Proc) + @normal = block + end + + # Sets the Proc to call for low notifications + # + def low=(block) + raise ArgumentError unless block.is_a?(Proc) + @low = block + end + + # Sets the email parameter + # + # + def email=(arg) + raise ArgumentError unless arg.is_a?(String) + @email = arg + end + + # Sets the sms parameter + # + # + def sms=(arg) + raise ArgumentError unless arg.is_a?(String) + @sms = arg + end + + # Sets the xmpp parameter + # + # + def xmpp=(arg) + raise ArgumentError unless arg.is_a?(String) + @xmpp = arg + end + + # Sets the password parameter + # + # + def password=(arg) + raise ArgumentError unless arg.is_a?(String) + @password=arg + end + # @return Log4r::Logger def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end @@ -39,8 +107,6 @@ module Mauve # @return [Boolean] def suppressed? ; @suppressed ; end - def holiday_url ; nil ; end - # Works out if a notification should be suppressed. If no parameters are supplied, it will # # @param [Time] Theoretical time of notification @@ -155,8 +221,7 @@ module Mauve # @param [Mauve::Alert] alert Alert we're notifiying about # # @return [Boolean] if the notification was successful - def send_alert(level, alert) - now = Time.now + def send_alert(level, alert, now=Time.now) was_suppressed = @suppressed @suppressed = self.should_suppress? @@ -168,7 +233,7 @@ module Mauve # We only suppress notifications if we were suppressed before we started, # and are still suppressed. # - if @suppressed or self.is_on_holiday? + if @suppressed or self.is_on_holiday?(now) or self.is_off_sick?(now) note = "#{alert.update_type.capitalize} notification to #{self.username} suppressed" logger.info note + " about #{alert}." History.create(:alerts => [alert], :type => "notification", :event => note) @@ -217,20 +282,19 @@ module Mauve end end - # - # - # - def alert_during - - end - # Whether the person is on holiday or not. # # @return [Boolean] True if person on holiday, false otherwise. - def is_on_holiday? () - return false if holiday_url.nil? or holiday_url.empty? + def is_on_holiday?(at=Time.now) + return false if self.notify_when_on_holiday + + return CalendarInterface.is_user_on_holiday?(self.username, at) + end + + def is_off_sick?(at=Time.now) + return false if self.notify_when_off_sick - return CalendarInterface.is_user_on_holiday?(holiday_url) + return CalendarInterface.is_user_off_sick?(self.username, at) end end diff --git a/lib/mauve/processor.rb b/lib/mauve/processor.rb index 0ac7b59..acf72b3 100644 --- a/lib/mauve/processor.rb +++ b/lib/mauve/processor.rb @@ -57,14 +57,11 @@ module Mauve to_delete = [] - @transmission_id_cache.each do |tid, received_at| - to_delete << tid if (now - received_at) > @transmission_cache_expire_time + @transmission_id_cache = @transmission_id_cache.delete_if do |cache_data| + tid, received_at = cache_data + (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 @@ -197,6 +194,11 @@ module Mauve sz.times do process_packet(*Server.packet_pop) end + + # + # Now expire the cache. This will only get processed at most once every minute. + # + expire_transmission_id_cache end def timer_should_stop? diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb index 2b0e101..366191c 100644 --- a/lib/mauve/server.rb +++ b/lib/mauve/server.rb @@ -50,21 +50,10 @@ module Mauve @notification_buffer = [] # - # Set the auth/calendar URLs + # Bank Holidays -- this list is kept here, because I can't think of + # anywhere else to put it. # - @bytemark_auth_url = nil - @bytemark_calendar_url = nil - - # - # Set a couple of params for remote HTTP requests. - # - @remote_http_timeout = 5 - @remote_https_verify_mode = OpenSSL::SSL::VERIFY_PEER - - # - # Rate limit login attempts to limit the success of brute-forcing. - # - @failed_login_delay = 1 + @bank_holidays = nil # # Set up a blank config. @@ -114,83 +103,6 @@ module Mauve @notification_buffer end - # Set the calendar URL. - # - # @param [String] arg - # @return [URI] - def bytemark_calendar_url=(arg) - raise ArgumentError, "bytemark_calendar_url must be a string" unless arg.is_a?(String) - - @bytemark_calendar_url = URI.parse(arg) - - # - # Make sure we get an HTTP URL. - # - raise ArgumentError, "bytemark_calendar_url must be an HTTP(S) URL." unless %w(http https).include?(@bytemark_calendar_url.scheme) - - # - # Set a default request path, if none was given - # - @bytemark_calendar_url.path="/" if @bytemark_calendar_url.path.empty? - - @bytemark_calendar_url - end - - # Set the Bytemark Authentication URL - # - # @param [String] arg - # @return [URI] - def bytemark_auth_url=(arg) - raise ArgumentError, "bytemark_auth_url must be a string" unless arg.is_a?(String) - - @bytemark_auth_url = URI.parse(arg) - # - # Make sure we get an HTTP URL. - # - raise ArgumentError, "bytemark_auth_url must be an HTTP(S) URL." unless %w(http https).include?(@bytemark_auth_url.scheme) - - # - # Set a default request path, if none was given - # - @bytemark_auth_url.path="/" if @bytemark_auth_url.path.empty? - - @bytemark_auth_url - end - - # Sets the timeout when making remote HTTP requests - # - # @param [Integer] arg - # @return [Integer] - def remote_http_timeout=(arg) - raise ArgumentError, "initial_sleep must be an integer" unless s.is_a?(Integer) - @remote_http_timeout = arg - end - - # Sets the SSL verification mode when makeing remote HTTPS requests - # - # @param [String] arg must be one of "none" or "peer" - # @return [Constant] - def remote_https_verify_mode=(arg) - @remote_https_verify_mode = case arg - when "peer" - OpenSSL::SSL::VERIFY_PEER - when "none" - OpenSSL::SSL::VERIFY_NONE - else - raise ArgumentError, "remote_https_verify_mode must be either 'peer' or 'none'" - end - end - - # Set the delay added following a failed login attempt. - # - # @param [Numeric] arg Number of seconds to delay following a failed login attempt - # @return [Numeric] - # - def failed_login_delay=(arg) - raise ArgumentError, "initial_sleep must be numeric" unless arg.is_a?(Numeric) - @failed_login_delay = arg - end - # Set the sleep period during which notifications about old alerts are # suppressed. # @@ -207,6 +119,24 @@ module Mauve Time.now < self.started_at + self.initial_sleep end + # Check with the calendar for the list of bank holidays + # + # + def bank_holidays + # + # Update the bank holidays list hourly. + # + if @bank_holidays.nil? or + @bank_holidays_last_checked_at.nil? or + @bank_holidays_last_checked_at < (Time.now - 1.hour) + + @bank_holidays = CalendarInterface.get_bank_holiday_list(Time.now) + @bank_holidays_last_checked_at = Time.now + end + + @bank_holidays + end + # return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) diff --git a/lib/mauve/web_interface.rb b/lib/mauve/web_interface.rb index 16e7da2..a8e1ece 100644 --- a/lib/mauve/web_interface.rb +++ b/lib/mauve/web_interface.rb @@ -218,9 +218,13 @@ EOF note = params[:note] || nil n_hours = (n_hours.to_f > 188 ? 188 : n_hours.to_f) + type_hours = "daytime" unless %w(daytime working wallclock).include?(type_hours) if ack_until.to_s.empty? - ack_until = Time.now.in_x_hours(n_hours, type_hours.to_s) + now = Time.now + now.bank_holidays = Server.instance.bank_holidays + + ack_until = now.in_x_hours(n_hours, type_hours.to_s) else ack_until = Time.at(ack_until.to_i) end @@ -275,12 +279,14 @@ EOF # n_hours = ( n_hours > 300 ? 300 : n_hours ) type_hours = "daytime" unless %w(daytime working wallclock).include?(type_hours) - ack_until = Time.now.in_x_hours(n_hours, type_hours) + now = Time.now + now.bank_holidays = Server.instance.bank_holidays + ack_until = now.in_x_hours(n_hours, type_hours) # # Make sure we can't ack longer than a week. # - max_ack = (Time.now + 86400*8) + max_ack = (Time.now + Configuration.current.max_acknowledgement_time) ack_until = max_ack if ack_until > max_ack # @@ -359,8 +365,13 @@ EOF type_hours = params[:type_hours].to_s note = params[:note] || nil + type_hours = "daytime" unless %w(daytime working wallclock).include?(type_hours) + if ack_until == 0 - ack_until = Time.now.in_x_hours(n_hours, type_hours) + now = Time.now + now.bank_holidays = Server.instance.bank_holidays + + ack_until = now.in_x_hours(n_hours, type_hours) else ack_until = Time.at(ack_until) end diff --git a/lib/object_builder.rb b/lib/object_builder.rb index ebd655b..ccb2c63 100644 --- a/lib/object_builder.rb +++ b/lib/object_builder.rb @@ -181,6 +181,20 @@ class ObjectBuilder end 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) + else + result.send(name.to_sym) + end + end + + class << self # Defines a new builder diff --git a/test/tc_mauve_alert.rb b/test/tc_mauve_alert.rb index 2b8af6f..3af9075 100644 --- a/test/tc_mauve_alert.rb +++ b/test/tc_mauve_alert.rb @@ -95,8 +95,7 @@ EOF end def test_acknowledge! - person = Mauve::Person.new - person.username = "test-user" + person = Mauve::Person.new("test-user") Server.instance.setup diff --git a/test/tc_mauve_authentication.rb b/test/tc_mauve_authentication.rb index d0f2d4f..c364aca 100644 --- a/test/tc_mauve_authentication.rb +++ b/test/tc_mauve_authentication.rb @@ -26,9 +26,7 @@ class TcMauveAuthentication < Mauve::UnitTest def test_default_auth_always_fails config=<<EOF -server { - failed_login_delay 0 -} +failed_login_delay 0 EOF Configuration.current = ConfigurationBuilder.parse(config) @@ -43,9 +41,7 @@ EOF def test_local_auth config=<<EOF -server { - failed_login_delay 0 -} +failed_login_delay 0 person ("test") { password "#{Digest::SHA1.new.hexdigest("password")}" @@ -70,9 +66,7 @@ EOF def test_local_auth config=<<EOF -server { - failed_login_delay 0 -} +failed_login_delay 0 person ("nopass") { } @@ -97,10 +91,8 @@ EOF # test2: POKvBqLT7 # config=<<EOF -server { - failed_login_delay 0 - bytemark_auth_url "https://auth.bytemark.co.uk/" -} +failed_login_delay 0 +bytemark_auth_url "https://auth.bytemark.co.uk/" person ("test1") { } diff --git a/test/tc_mauve_calendar_interface.rb b/test/tc_mauve_calendar_interface.rb new file mode 100644 index 0000000..ddf4039 --- /dev/null +++ b/test/tc_mauve_calendar_interface.rb @@ -0,0 +1,155 @@ +$:.unshift "../lib" + +require 'th_mauve' +require 'mauve/calendar_interface' +require 'mauve/configuration_builder' +require 'mauve/configuration' +require 'webmock' +# +# Ugh webmock is annoying. +WebMock.allow_net_connect! + +class TcMauveCalendarInterface < Mauve::UnitTest + + include WebMock::API + include Mauve + + def setup + WebMock.disable_net_connect! + super + end + + def teardown + WebMock.reset! + WebMock.allow_net_connect! + super + end + + def test_get_attendees + attendees = %w(test1 test2) + stub_request(:get, "http://localhost/calendar/api/attendees/support_shift/2011-08-01T00:00:00"). + to_return(:status => 200, :body => YAML.dump(attendees)) + + config =<<EOF +bytemark_calendar_url "http://localhost/calendar" +EOF + + Configuration.current = ConfigurationBuilder.parse(config) + + assert_equal(attendees, CalendarInterface.get_attendees("support_shift")) + end + + def test_is_user_on_holiday? + attendees = %w(test1 test2) + stub_request(:get, "http://localhost/calendar/api/attendees/staff_holiday/2011-08-01T00:00:00"). + to_return(:status => 200, :body => YAML.dump(attendees)) + + + config =<<EOF +bytemark_calendar_url "http://localhost/calendar" +EOF + + Configuration.current = ConfigurationBuilder.parse(config) + + assert(CalendarInterface.is_user_on_holiday?("test1")) + assert(!CalendarInterface.is_user_on_holiday?("test3")) + end + + def test_is_user_off_sick? + attendees = %w(test1 test2) + stub_request(:get, "http://localhost/calendar/api/attendees/sick_period/2011-08-01T00:00:00"). + to_return(:status => 200, :body => YAML.dump(attendees)) + + config =<<EOF +bytemark_calendar_url "http://localhost/calendar" +EOF + + Configuration.current = ConfigurationBuilder.parse(config) + + assert(CalendarInterface.is_user_off_sick?("test1")) + assert(!CalendarInterface.is_user_off_sick?("test3")) + end + + def test_do_get + url = "http://localhost/" + + # + # This sets up two redirects, followed by the answer (below) + # + 2.times do |x| + next_url = url + "#{x}/" + stub_request(:get, url). + to_return(:status => 301, :body => nil, :headers => {:location => next_url}) + url = next_url + end + + # + # And finally the answer. + # + stub_request(:get, url). + to_return(:status => 200, :body => "OK!", :headers => {}) + + # + # Now do_get should return "OK!" when the maximum number of redirects is set to two. + # + result = nil + assert_nothing_raised{ result = CalendarInterface.__send__(:do_get, "http://localhost/", 2) } + assert_equal("OK!",result) + + # + # do_get should return nil when the maximum number of redirects is set to two. + # + assert_nothing_raised{ result = CalendarInterface.__send__(:do_get, "http://localhost/", 1) } + assert_nil(result) + + # + # Pop the warning about the redirect off the end of the log. + # + logger_pop + end + + def test_do_get_with_cache + url = "http://localhost/" + + # + # This stubs the request to give out the time + # + stub_request(:get, url). + to_return( lambda{ {:status => 200, :body => YAML.dump(Time.now), :headers => {}} } ) + + # + # This reponse should not be cached, the cache-until paramter is "now" + # + assert_equal(Time.now, CalendarInterface.__send__(:do_get_with_cache, url, Time.now)) + + # + # Since the last request wasn't cached, the next one should give back + # "now", and should be cached for the next 10 seconds. + # + Timecop.freeze(Time.now + 5) + assert_equal(Time.now, CalendarInterface.__send__(:do_get_with_cache, url, Time.now + 10)) + + # + # This should have been cached from the last query. + # + Timecop.freeze(Time.now + 5) + assert_equal(Time.now - 5, CalendarInterface.__send__(:do_get_with_cache, url, Time.now + 10)) + + # + # Finally, this should now have expired from the cache. + # + Timecop.freeze(Time.now + 5) + assert_equal(Time.now, CalendarInterface.__send__(:do_get_with_cache, url, Time.now + 10)) + + Timecop.freeze(Time.now + 50) + cache = CalendarInterface.__send__(:clean_cache) + assert(cache.empty?) + end + + +end + + + + + diff --git a/test/tc_mauve_notification.rb b/test/tc_mauve_notification.rb index a447207..d0c2bab 100644 --- a/test/tc_mauve_notification.rb +++ b/test/tc_mauve_notification.rb @@ -442,9 +442,15 @@ people_list("testers", %w(test1 test2)) { alert_group("test") { level URGENT + notify("test1") notify("test2") notify("testers") + + notify("testers") { + every 60 + during { hours_in_day (3) } + } } EOF diff --git a/test/tc_mauve_people_list.rb b/test/tc_mauve_people_list.rb index 2d5da6d..fd8bda9 100644 --- a/test/tc_mauve_people_list.rb +++ b/test/tc_mauve_people_list.rb @@ -117,4 +117,29 @@ EOF end end + + def test_dynamic_people_list + # + # Allows us to pick up notifications sent. + # + $sent_notifications = [] + + config =<<EOF + +person "test1" + +person "test2" + +# +# This should oscillate between test1 and test2. +# +people_list "testers", lambda { $ans = ($ans == "test1" ? "test2" : "test1") } + +EOF + Configuration.current = ConfigurationBuilder.parse(config) + people_list = Configuration.current.people_lists["testers"] + assert_equal([Configuration.current.people["test1"]], people_list.people) + assert_equal([Configuration.current.people["test2"]], people_list.people) + end + end diff --git a/test/tc_mauve_time.rb b/test/tc_mauve_time.rb index 7a8fefa..6e5989b 100644 --- a/test/tc_mauve_time.rb +++ b/test/tc_mauve_time.rb @@ -35,8 +35,15 @@ class TestMauveTime < Mauve::UnitTest assert_equal(hour_1, t.in_x_hours(1,"working")) assert_equal(hour_0, t.in_x_hours(0,"working")) + end + + def test_bank_holiday? + x = Time.now + assert(!x.bank_holiday?) + x.bank_holidays << Date.new(x.year, x.month, x.day) + assert(x.bank_holiday?) end diff --git a/test/th_mauve.rb b/test/th_mauve.rb index ce29e11..99eaaed 100644 --- a/test/th_mauve.rb +++ b/test/th_mauve.rb @@ -55,6 +55,7 @@ module Mauve def setup reset_all_singletons + reset_mauve_configuration setup_logger setup_time end @@ -108,6 +109,10 @@ module Mauve Timecop.return end + def reset_mauve_configuration + Mauve::Configuration.current = Mauve::Configuration.new + end + def reset_all_singletons Mauve.constants.collect{|const| Mauve.const_get(const)}.each do |klass| next unless klass.respond_to?(:instance) |