diff options
author | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-06-09 18:09:52 +0100 |
---|---|---|
committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-06-09 18:09:52 +0100 |
commit | 495c44445642cfae8f23fadde299ad5307f5be58 (patch) | |
tree | 0104c9eef164235aa5ab05b126c8f63e52fb8624 | |
parent | 0c88fcc91db1b003cd5d5311f62700c7867b4099 (diff) |
Big commit
--HG--
rename : views/please_authenticate.haml => views/login.haml
37 files changed, 1263 insertions, 489 deletions
@@ -0,0 +1,5 @@ +aaab1eab14333f0de39bf1a6763ecfc70bb04a75 0.13 +f552a7440699fbe85523bf97e334cdee627773b3 0.17 +83f5ecdbdcd8c93c7e38780887a968642b924974 1.0.0.beta +429621063b4e3833832b2ee2cc637178d72e9125 1.0.beta2 +0f9cca9cb958c339e9e3c987909f2abe10911824 Jruby compatible version diff --git a/bin/mauveclient b/bin/mauveclient index 1c90741..b51d687 100755 --- a/bin/mauveclient +++ b/bin/mauveclient @@ -162,7 +162,6 @@ if help exit 0 end - error "No alerts specified" unless !update.alert.empty? || update.replace update.transmission_id = rand(2**63) diff --git a/bytemark_example_alerts.sh b/bytemark_example_alerts.sh index e0d9205..6e90c5c 100755 --- a/bytemark_example_alerts.sh +++ b/bytemark_example_alerts.sh @@ -1,17 +1,17 @@ #!/bin/sh -PRE="./mauve_starter.rb ./bin/mauvesend 127.0.0.1 " +PRE="ruby -I lib ./bin/mauveclient 127.0.0.1 " $PRE -o supportbot -i 173123 \ - -s "My server is not responding" \ - -d "<strong>From:</strong> John Smith <john@smith.name><br/> -<strong>To:</strong> support@support.bytemark.co.uk</br/> -<br/> -<pre>It has been several hours now since I have been able to contact my server -foo.bar.bytemark.co.uk. I am very upset that blah blah blah blah -and furthermore by business is under threat because £15.00 per month -is far too much blah blah blah</pre> -" + -s "My server is not responding" \ + -d "<strong>From:</strong> John Smith <john@smith.name><br/> +#<strong>To:</strong> support@support.bytemark.co.uk</br/> +#<br/> +#<pre>It has been several hours now since I have been able to contact my server +#foo.bar.bytemark.co.uk. I am very upset that blah blah blah blah +#and furthermore by business is under threat because £15.00 per month +#is far too much blah blah blah</pre> +#" $PRE -o networkmonitor -i 1 -u cr01.man.bytemark.co.uk \ -s "cr01.man.bytemark.co.uk did not respond to pings" @@ -23,8 +23,10 @@ $PRE -o networkmonitor -i 2 -u cr01.thn.bytemark.co.uk \ $PRE -o vmhs -i 12346 -u ventham.bytemark.co.uk \ -s "ventham.bytemark.co.uk heartbeat not received" -r +5 + $PRE -o vmhs -i 12345 -u partridge.bytemark.co.uk \ - -s "partridge.bytemark.co.uk heartbeat not received" -r +2 + -s "partridge.bytemark.co.uk heartbeat not received" -r +10 -c now + $PRE -o vmhs -i 12347 -u eider.bytemark.co.uk \ -s "eider.bytemark.co.uk heartbeat not received" -r +2 diff --git a/debian/changelog b/debian/changelog index d8b7263..cdd758a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +mauvealert (3.0.3) stable; urgency=low + + * Added get_all back to front-end + * Fixed up logging for more classes + * Catch empty notification lists + * Added console to server package + * Fixed document_root location + + -- Patrick J Cherry <patrick@bytemark.co.uk> Tue, 19 Apr 2011 11:31:10 +0100 + mauvealert (3.0.2) stable; urgency=low * Now checks for transmission time set to zero. diff --git a/debian/mauvealert-server.install b/debian/mauvealert-server.install index 47a3add..c23596d 100644 --- a/debian/mauvealert-server.install +++ b/debian/mauvealert-server.install @@ -1,4 +1,5 @@ bin/mauveserver usr/sbin/ +bin/mauveconsole usr/sbin/ lib/dm-sqlite-adapter-with-mutex.rb usr/lib/ruby/1.8/ lib/mauve/alert.rb usr/lib/ruby/1.8/mauve/ lib/mauve/alert_changed.rb usr/lib/ruby/1.8/mauve/ diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb index 30d50bb..b98866c 100644 --- a/lib/mauve/alert.rb +++ b/lib/mauve/alert.rb @@ -103,7 +103,7 @@ module Mauve def logger Log4r::Logger.new(self.class.to_s) end - + def time_relative(secs) secs = secs.to_i.abs case secs @@ -167,6 +167,10 @@ module Mauve def alert_group AlertGroup.matches(self)[0] end + + def level + self.alert_group.level + end def subject attribute_get(:subject) || source @@ -174,7 +178,8 @@ module Mauve def subject=(subject); set_changed_if_different(:subject, subject); end def summary=(summary); set_changed_if_different(:summary, summary); end - def detail=(detail); set_changed_if_different(:detail, detail); end +# def detail=(detail); set_changed_if_different(:detail, detail); end + def detail=(detail); attribute_set(:detail, detail) ; end protected def set_changed_if_different(attribute, value) @@ -252,10 +257,33 @@ module Mauve class << self - def all_current - all(:cleared_at => nil) + def all_raised + all(:raised_at.not => nil, :cleared_at => nil) end - + + def all_acknowledged + all(:acknowledged_at.not => nil) + end + + def all_cleared + all(:cleared_at.not => nil) + end + + # Returns a hash of all the :urgent, :normal and :low alerts. + # + # @return [Hash] A hash with the relevant alerts per level + def get_all () + hash = Hash.new + hash[:urgent] = Array.new + hash[:normal] = Array.new + hash[:low] = Array.new + all().each do |iter| + next if true == iter.cleared? + hash[AlertGroup.matches(iter)[0].level] << iter + end + return hash + end + # Returns the next Alert that will have a timed action due on it, or nil # if none are pending. # @@ -273,8 +301,9 @@ module Mauve # Receive an AlertUpdate buffer from the wire. # def receive_update(update, reception_time = MauveTime.now) - update = Proto::AlertUpdate.parse_from_string(update) unless - update.kind_of?(Proto::AlertUpdate) + + update = Proto::AlertUpdate.parse_from_string(update) unless update.kind_of?(Proto::AlertUpdate) + alerts_updated = [] logger.debug("Alert update received from wire: #{update.inspect.split.join(", ")}") @@ -289,7 +318,8 @@ module Mauve end time_offset = (reception_time - transmission_time).round - logger.debug("Update received from a host #{time_offset}s behind") if time_offset.abs > 0 + + logger.debug("Update received from a host #{time_offset}s behind") if time_offset.abs > 5 # Update each alert supplied # @@ -297,8 +327,17 @@ module Mauve # Infer some actions from our pure data structure (hmm, wonder if # this belongs in our protobuf-derived class? # - raise_time = alert.raise_time == 0 ? nil : MauveTime.at(alert.raise_time + time_offset) clear_time = alert.clear_time == 0 ? nil : MauveTime.at(alert.clear_time + time_offset) + raise_time = alert.raise_time == 0 ? nil : MauveTime.at(alert.raise_time + time_offset) + + if raise_time.nil? && clear_time.nil? + # + # Make sure that we raise if neither raise nor clear is set + # + logger.warn("No clear time or raise time set. Assuming raised!") + + raise_time = reception_time + end logger.debug("received at #{reception_time}, transmitted at #{transmission_time}, raised at #{raise_time}, clear at #{clear_time}") @@ -316,10 +355,10 @@ module Mauve ## # - # Allow a 15s offset in timings. + # Allow a 5s offset in timings. # if raise_time - if raise_time <= (reception_time + 15) + if raise_time <= (reception_time + 5) alert_db.raised_at = raise_time else alert_db.will_raise_at = raise_time @@ -327,19 +366,24 @@ module Mauve end if clear_time - if clear_time <= (reception_time + 15) + if clear_time <= (reception_time + 5) alert_db.cleared_at = clear_time else alert_db.will_clear_at = clear_time end end - # re-raise + # + # Re-raise if raised_at and cleared_at are set. + # if alert_db.cleared_at && alert_db.raised_at && alert_db.cleared_at < alert_db.raised_at alert_db.cleared_at = nil end - if pre_cleared && alert_db.raised? + # + # + # + if (pre_raised or pre_cleared) && alert_db.raised? alert_db.update_type = :raised elsif pre_raised && alert_db.cleared? alert_db.update_type = :cleared @@ -355,8 +399,8 @@ module Mauve # These updates happen but do not sent the alert back to the # notification system. # - alert_db.importance = alert.importance if alert.importance != 0 - + alert_db.importance = alert.importance if alert.importance != 0 + # FIXME: this logic ought to be clearer as it may get more complicated # if alert_db.update_type @@ -369,6 +413,8 @@ module Mauve alert_db.update_type = :changed end + logger.debug "Saving #{alert_db}" + if !alert_db.save if alert_db.errors.respond_to?("full_messages") msg = alert_db.errors.full_messages @@ -394,7 +440,9 @@ module Mauve alerts_updated << alert_db end end - + + logger.debug "Got #{alerts_updated.length} alerts to notify about" + AlertGroup.notify(alerts_updated) end diff --git a/lib/mauve/alert_group.rb b/lib/mauve/alert_group.rb index d8156fa..288b263 100644 --- a/lib/mauve/alert_group.rb +++ b/lib/mauve/alert_group.rb @@ -29,11 +29,14 @@ module Mauve logger.warn "no groups found for #{alert.id}" if groups.empty? # - # Notify each group. + # Notify just the group that thinks this alert is the most urgent. # - groups.each do |grp| - logger.info("notifying group #{groups[0]} of AlertID.#{alert.id}.") - grp.notify(alert) + %w(urgent normal low).each do |lvl| + this_group = groups.find{|grp| grp.level.to_s == lvl} + next if this_group.nil? + logger.info("notifying group #{this_group} of AlertID.#{alert.id} (matching #{lvl})") + this_group.notify(alert) + break end end end diff --git a/lib/mauve/auth_bytemark.rb b/lib/mauve/auth_bytemark.rb index 7419d10..9e0a6d1 100644 --- a/lib/mauve/auth_bytemark.rb +++ b/lib/mauve/auth_bytemark.rb @@ -3,7 +3,7 @@ require 'sha1' require 'xmlrpc/client' require 'timeout' -class AuthSourceBytemark +class 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 @@ -11,6 +11,7 @@ class AuthSourceBytemark @srv = srv @port = port @timeout = 7 + @logger = Log4r::Logger.new(self.class.to_s) end ## Not really needed. @@ -33,15 +34,16 @@ class AuthSourceBytemark raise ArgumentError.new("Login must be a string, not a #{login.class}") if String != login.class raise ArgumentError.new("Password must be a string, not a #{password.class}") if String != password.class raise ArgumentError.new("Login or/and password is/are empty.") if login.empty? || password.empty? + client = XMLRPC::Client.new(@srv,"/",@port,nil,nil,nil,nil,true,@timeout).proxy("bytemark.auth") + begin challenge = client.getChallengeForUser(login) response = Digest::SHA1.new.update(challenge).update(password).hexdigest client.login(login, response) - rescue XMLRPC::FaultException => fault - return "Fault code is #{fault.faultCode} stating #{fault.faultString}" + rescue Exception => ex + return false end - return true end end diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb index 08cfab3..ab2bc5b 100644 --- a/lib/mauve/calendar_interface.rb +++ b/lib/mauve/calendar_interface.rb @@ -27,7 +27,7 @@ module Mauve # @return [Array] A list of all the username on support. def self.get_users_on_support(url) result = get_URL(url) - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" logger.debug("Cheching who is on support: #{result}") return result end @@ -40,7 +40,7 @@ module Mauve # @param [String] usr User single sign on. # @return [Boolean] True if on support, false otherwise. def self.is_user_on_support?(url, usr) - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" list = get_URL(url) if true == list.include?("nobody") logger.error("Nobody is on support thus alerts are ignored.") @@ -63,7 +63,7 @@ module Mauve return false if true == list.nil? or true == list.empty? pattern = /[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}:[\d]{2}/ result = (list[0].match(pattern))? true : false - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" logger.debug("Cheching if #{usr} is on holiday: #{result}") return result end @@ -83,7 +83,7 @@ module Mauve # @retur [Array] An array of strings, each newline creates an new item. def self.get_URL (uri_str, limit = 11) - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" if 0 == limit logger.warn("HTTP redirect deeper than 11 on #{uri_str}.") diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb index b11e1b5..4b7717d 100644 --- a/lib/mauve/configuration.rb +++ b/lib/mauve/configuration.rb @@ -212,6 +212,7 @@ module Mauve is_attribute "port" is_attribute "ip" is_attribute "document_root" + is_attribute "session_secret" def builder_setup @result = HTTPServer.instance @@ -322,9 +323,17 @@ module Mauve end def holiday_url (url) - @result.holiday_url = url + @result.holiday_url = url.to_s end - + + def email(e) + @result.email = e.to_s + end + + def xmpp(x) + @result.xmpp = x.to_s + end + def suppress_notifications_after(h) raise ArgumentError.new("notification_threshold must be specified as e.g. (10 => 1.minute)") unless h.kind_of?(Hash) && h.keys[0].kind_of?(Integer) && h.values[0].kind_of?(Integer) @@ -384,7 +393,6 @@ module Mauve # Create a new instance and adds it. def builder_setup(label) - pp label @result = PeopleList.new(label) end diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb index 69b566b..4fd8b60 100644 --- a/lib/mauve/http_server.rb +++ b/lib/mauve/http_server.rb @@ -2,15 +2,15 @@ # # Bleuurrgggggh! Bleurrrrrgghh! # +require 'mauve/auth_bytemark' +require 'mauve/web_interface' +require 'mauve/mauve_thread' require 'digest/sha1' require 'log4r' require 'thin' require 'rack' require 'rack-flash' require 'rack/handler/webrick' -require 'mauve/auth_bytemark' -require 'mauve/web_interface' -require 'mauve/mauve_thread' ################################################################################ # @@ -87,19 +87,19 @@ module Mauve attr_accessor :session_secret # not used yet def initialize - @port = 32761 + @port = 1288 @ip = "127.0.0.1" - @document_root = "." - @session_secret = rand(2**100).to_s + @document_root = "/usr/share/mauvealert" + @session_secret = "%x" % rand(2**100) end def main_loop - @server = ::Thin::Server.new(@ip, @port, Rack::CommonLogger.new(Rack::Chunked.new(Rack::ContentLength.new(WebInterface.new)), RackErrorsProxy.new(logger)), :signals => false) + @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 691200}), :signals => false) @server.start end def stop - @server.stop + @server.stop if @server super end end diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb index 2220211..02bf6fd 100644 --- a/lib/mauve/notification.rb +++ b/lib/mauve/notification.rb @@ -31,7 +31,7 @@ module Mauve @time = time @alert = alert @during = during || Proc.new { true } - @logger = Log4r::Logger.new "mauve::DuringRunner" + @logger = Log4r::Logger.new "Mauve::DuringRunner" end def now? @@ -137,6 +137,11 @@ module Mauve # def alert_changed(alert) + if people.nil? or people.empty? + logger.warn "No people found in for notification #{list}" + return + end + # Should we notificy at all? is_relevant = DuringRunner.new(MauveTime.now, alert, &during).now? diff --git a/lib/mauve/notifier.rb b/lib/mauve/notifier.rb index e0692f6..0127b6b 100644 --- a/lib/mauve/notifier.rb +++ b/lib/mauve/notifier.rb @@ -40,11 +40,38 @@ module Mauve def start super - Configuration.current.notification_methods['xmpp'].connect if Configuration.current.notification_methods['xmpp'] + if Configuration.current.notification_methods['xmpp'] + # + # Connect to XMPP server + # + xmpp = Configuration.current.notification_methods['xmpp'] + xmpp.connect + + Configuration.current.people.each do |username, person| + # + # Ignore people without XMPP stanzas. + # + next unless person.xmpp + + # + # 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) + else + xmpp.ensure_roster_and_subscription!(person.xmpp) + end + + Configuration.current.people[username].xmpp = jid unless jid.nil? + end + end end def stop - Configuration.current.notification_methods['xmpp'].close + if Configuration.current.notification_methods['xmpp'] + Configuration.current.notification_methods['xmpp'].close + end super end diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb index 2c14a54..f3b9a0f 100644 --- a/lib/mauve/notifiers/email.rb +++ b/lib/mauve/notifiers/email.rb @@ -38,6 +38,11 @@ module Mauve @suppressed_changed = nil end + def logger + @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") + + end + def send_alert(destination, alert, all_alerts, conditions = nil) message = prepare_message(destination, alert, all_alerts, conditions) args = [@server, @port] @@ -46,14 +51,11 @@ module Mauve Net::SMTP.start(*args) do |smtp| smtp.send_message(message, @from, destination) end - rescue Errno::ECONNREFUSED => e - @logger = Log4r::Logger.new "mauve::email_send_alert" - @logger.error("#{e.class}: #{e.message} raised. " + - "args = #{args.inspect} " - ) - raise e - rescue => e - raise e + true + rescue StandardError => ex + logger.error "SMTP failure: #{ex.to_s}" + logger.debug ex.backtrace.join("\n") + false end end @@ -98,8 +100,8 @@ module Mauve # FIXME: include alert.detail as multipart mime ##Thread.abort_on_exception = true m.body += "\n" + '-'*10 + " This is the detail field " + '-'*44 + "\n\n" - m.body += alert.get_details() - m.body += alert.get_details_plain_text() + m.body += alert.detail.to_s +#' m.body += alert.get_details_plain_text() m.body += "\n" + '-'*80 + "\n\n" if @suppressed_changed == true diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb index 18df6b2..fbc9640 100644 --- a/lib/mauve/notifiers/xmpp.rb +++ b/lib/mauve/notifiers/xmpp.rb @@ -1,68 +1,106 @@ require 'log4r' require 'xmpp4r' -require 'xmpp4r/xhtml' require 'xmpp4r/roster' -require 'xmpp4r/muc/helper/simplemucclient' +require 'xmpp4r/muc' +# require 'xmpp4r/xhtml' +# require 'xmpp4r/discovery/helper/helper' require 'mauve/notifiers/debug' -#Jabber::debug = true -module Mauve - module Notifiers - module Xmpp - - class CountingMUCClient < Jabber::MUC::SimpleMUCClient - attr_reader :participants - def initialize(*a) - super(*a) - @participants = 0 - self.on_join { @participants += 1 } - self.on_leave { @participants -= 1 } - end +# +# A couple of monkey patches to fix up all this nonsense. +# +module Jabber + class Stream + def close + # + # Just close + # + close! + end + def close! + 10.times do + pr = 0 + @tbcbmutex.synchronize { pr = @processing } + break if pr = 0 + Thread::pass if pr > 0 + sleep 1 end + + # Order Matters here! If this method is called from within + # @parser_thread then killing @parser_thread first would + # mean the other parts of the method fail to execute. + # That would be bad. So kill parser_thread last + @tbcbmutex.synchronize { @processing = 0 } + @fd.close if @fd and !@fd.closed? + @status = DISCONNECTED + stop + end + end +end + + + + +module Mauve + module Notifiers + module Xmpp +# class CountingMUCClient < Jabber::MUC::SimpleMUCClient +# +# attr_reader :participants +# +# def initialize(*a) +# super(*a) +# @participants = 0 +# self.on_join { @participants += 1 } +# self.on_leave { @participants -= 1 } +# end +# +# end +# class Default include Jabber - + # Atrtribute. attr_reader :name # Atrtribute. attr_accessor :jid, :password - # Atrtribute. - attr_accessor :initial_jid - - # Atrtribute. - attr_accessor :initial_messages - def initialize(name) + Jabber::logger = self.logger + #Jabber::debug = true + #Jabber::warnings = true + @name = name @mucs = {} @roster = nil + @closing = false + end def logger - @logger ||= Log4r::Logger.new self.class.to_s + # Give the logger a sane name + @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") + end + + def jid=(jid) + @jid = JID.new(jid) end + + def connect + logger.info "Jabber starting connection to #{@jid}" - def reconnect - if @client - begin - logger.debug "Jabber closing old client connection" - @client.close - @client = nil - @roster = nil - rescue Exception => ex - logger.error "#{ex} when reconnecting" - end - end + # Make sure we're disconnected. + self.close if @client.is_a?(Client) + + @client = Client.new(@jid) - logger.debug "Jabber starting connection to #{@jid}" - @client = Client.new(JID::new(@jid)) + @closing = false @client.connect @client.auth_nonsasl(@password, false) @roster = Roster::Helper.new(@client) @@ -70,11 +108,17 @@ module Mauve # Unconditionally accept all roster add requests, and respond with a # roster add + subscription request of our own if we're not subscribed # already - @roster.add_subscription_request_callback do |ri, stanza| + @roster.add_subscription_request_callback do |ri, presence| Thread.new do - logger.debug("Accepting subscription request from #{stanza.from}") - @roster.accept_subscription(stanza.from) - ensure_roster_and_subscription!(stanza.from) + logger.debug "Known? #{is_known_contact?(presence.from).inspect}" + if is_known_contact?(presence.from) + logger.info("Accepting subscription request from #{presence.from}") + @roster.accept_subscription(presence.from) + ensure_roster_and_subscription!(presence.from) + else + logger.info("Declining subscription request from #{presence.from}") + @roster.decline_subscription(presence.from) + end end.join end @@ -85,37 +129,40 @@ module Mauve @roster.wait_for_roster logger.debug "Jabber authenticated, setting presence" - @client.send(Presence.new.set_type(:available)) - - @mucs = {} - - logger.debug "Jabber is ready in theory" - end - - def reconnect_and_retry_on_error - @already_reconnected = false - begin - yield - rescue StandardError => ex - logger.error "#{ex} during notification\n" - logger.debug ex.backtrace - if !@already_reconnected - reconnect - @already_reconnected = true - retry - else - raise ex + @client.send(Presence.new(nil, "Woo!").set_type(nil)) + + @client.on_exception do |ex, stream, where| + # + # The XMPP4R exception clauses in Stream all close the stream, so + # we just need to reconnect. + # + unless ex.nil? or @closing + logger.warn(["Caught",ex.class,ex.to_s,"during XMPP",where].join(" ")) + connect + @mucs.each do |jid, muc| + @mucs.delete(jid) + join_muc(jid) + end end end end - def connect - self.reconnect_and_retry_on_error { self.send_msg(@initial_jid, "Hello!") } + # + # Kills the processor thread + # + def stop + @client.stop end def close - self.send_msg(@initial_jid, "Goodbye!") - @client.close + @closing = true + if @client and @client.is_connected? + @mucs.each do |jid, muc| + muc.exit("Goodbye!") if muc.active? + end + @client.send(Presence.new(nil, "Goodbye!").set_type(:unavailable)) + @client.close! + end end # Takes an alert and converts it into a message. @@ -144,97 +191,232 @@ module Mauve # or more of the choices - see +check_jid_has_presence+ for options. def send_alert(destination, alert, all_alerts, conditions = nil) - #message = Message.new(nil, alert.summary_two_lines.join("\n")) - message = Message.new(nil, convert_alert_to_message(alert)) - + destination_jid = JID.new(destination) + if conditions @suppressed_changed = conditions[:suppressed_changed] end - # MUC JIDs are prefixed with muc: - we need to strip this out. - destination_is_muc, dest_jid = self.is_muc?(destination) - - begin - xhtml = XHTML::HTML.new("<p>" + - convert_alert_to_message(alert)+ -# alert.summary_three_lines.join("<br />") + - #alert.summary_two_lines.join("<br />") + - "</p>") - message.add_element(xhtml) - rescue REXML::ParseException => ex - logger.warn("Can't send XMPP alert as valid XHTML-IM, falling back to plaintext") - logger.debug(ex) - end - - logger.debug "Jabber sending #{message} to #{destination}" - reconnect unless @client - - ensure_roster_and_subscription!(dest_jid) unless destination_is_muc - - if conditions && !check_alert_conditions(dest_jid, conditions) - logger.debug("Alert conditions not met, not sending XMPP alert to #{jid}") + if conditions && !check_alert_conditions(destination_jid, conditions) + logger.info("Alert conditions not met, not sending XMPP alert to #{destination_jid}") return false end - if destination_is_muc - if !@mucs[dest_jid] - @mucs[dest_jid] = CountingMUCClient.new(@client) - @mucs[dest_jid].join(JID.new(dest_jid)) - end - reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } - else - message.to = dest_jid - reconnect_and_retry_on_error { @client.send(message) ; true } - end + send_message(destination_jid, convert_alert_to_message(alert)) end # Sends a message to the destionation. # - # @param [String] destionation The (full) JID to send to. + # @param [String] destination The (full) JID to send to. # @param [String] msg The (formatted) message to send. # @return [NIL] nada. - def send_msg(destination, msg) - reconnect unless @client - message = Message.new(nil, msg) - destination_is_muc, dest_jid = self.is_muc?(destination) - if destination_is_muc - if !@mucs[dest_jid] - @mucs[dest_jid] = CountingMUCClient.new(@client) - @mucs[dest_jid].join(JID.new(dest_jid)) + def send_message(jid, msg) + jid = JID.new(jid) unless jid.is_a?(JID) + + message = Message.new(jid) + + #if msg.is_a?(XHTML::HTML) + # message.add_element(msg) + #else + message.body = msg + #end + + if is_muc?(jid) + jid = join_muc(jid.strip) + muc = @mucs[jid] + + if muc + message.to = muc.jid + muc.send(message) + true + else + logger.warn "Failed to join MUC #{jid} when trying to send a message" + false end - reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } else - message.to = dest_jid - reconnect_and_retry_on_error { @client.send(message) ; true } + # + # We aren't interested in sending things to people who aren't online. + # + ensure_roster_and_subscription!(jid) + + if check_jid_has_presence(jid) + # + # We set the chat type to chat + # + message.type = :chat + message.to = jid + @client.send(message) + true + else + false + end end - return nil end - protected + # + # Joins a chat, and returns the stripped JID of the chat joined. + # + def join_muc(jid, password=nil) + if jid.is_a?(String) + jid = JID.new($1) if jid =~ /^muc:(.*)/ + end + + unless jid.is_a?(JID) + logger.warn "I don't think #{jid} is a MUC" + return + end + + jid.resource = @client.jid.resource if jid.resource.to_s.empty? + + if !@mucs[jid.strip] + + logger.info("Adding new MUC client for #{jid}") + + @mucs[jid.strip] = Jabber::MUC::MUCClient.new(@client) + + # Add some callbacks + @mucs[jid.strip].add_message_callback do |m| + receive_message(m) + end + + @mucs[jid.strip].add_private_message_callback do |m| + receive_message(m) + end + + end + + if !@mucs[jid.strip].active? + logger.info("Joining #{jid}") + # + # Make sure we have a resource. + # + @mucs[jid.strip].join(jid, password) - # Checks whether the destination JID is a MUC. - # Returns [true/false, destination] - def is_muc?(destination) - if /^muc:(.*)/.match(destination) - [true, $1] else - [false, destination] + logger.debug("Already joined #{jid}.") end - end + + # + # Return the JID object + # + jid.strip + end + # + # Checks whether the destination JID is a MUC. + # + def is_muc?(jid) + (jid.is_a?(JID) and @mucs.keys.include?(jid.strip)) or + (jid.is_a?(String) and jid =~ /^muc:(.*)/) + + # + # It would be nice to use service discovery to determin this, but it + # turns out that it is shite in xmpp4r. It doesn't return straight + # away with an answer, making it a bit useless. Some sort of weird + # threading issue, I think. + # + # begin + # logger.warn caller.join("\n") + # cl = Discovery::Helper.new(@client) + # res = cl.get_info_for(jid.strip) + # @client.wait + # logger.warn "hello #{res.inspect}" + # res.is_a?(Discovery::IqQueryDiscoInfo) and res.identity.category == :conference + # rescue Jabber::ServerError => ex + # false + # end + end + + # # Checks to see if the JID is in our roster, and whether we are # subscribed to it or not. Will add to the roster and subscribe as # is necessary to ensure both are true. + # def ensure_roster_and_subscription!(jid) - jid = JID.new(jid) - ri = @roster.find(jid)[jid] - if ri.nil? - @roster.add(jid, nil, true) - else - ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) - end - rescue Exception => ex + return jid if is_muc?(jid) + + jid = JID.new(jid) unless jid.is_a?(JID) + + ri = @roster.find(jid).values.first + @roster.add(jid, nil, true) if ri.nil? + + ri = @roster.find(jid).values.first + ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) + ri.jid + rescue StandardError => ex logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}") + nil + end + + protected + + def receive_message(msg) + # We only want to hear messages from known contacts. + unless is_known_contact?(msg.from) + # ignore message + logger.info "Ignoring message from unknown contact #{msg.from}" + return nil + end + + case msg.type + when :error + receive_error_message(msg) + when :groupchat + receive_groupchat_message(msg) + else + receive_normal_message(msg) + end + end + + def receive_error_message(msg) + logger.warn("Caught XMPP error #{msg}") + nil + end + + def receive_normal_message(msg) + # + # Treat invites specially + # + if msg.x("jabber:x:conference") + # + # recieved an invite. Need to mangle the jid. + # + jid =JID.new(msg.x("jabber:x:conference").attribute("jid")) + # jid.resource = @client.jid.resource + logger.info "Received an invite to #{jid}" + unless join_muc(jid) + logger.warn "Failed to join MUC #{jid} following invitation" + return nil + end + elsif msg.body + # + # Received a message with a body. + # + jid = msg.from + end + + # + # I don't have time to talk to myself! + # + if jid and jid.strip != @client.jid.strip + txt = File.executable?('/usr/games/fortune') ? `/usr/games/fortune -s -n 60`.chomp : "I'd love to stay and chat, but I'm really too busy." + send_message(jid, txt) + end + end + + def receive_groupchat_message(msg) + # + # We only want group chat messages from MUCs we're already joined to, + # that we've not sent ourselves, that are not historical, and that + # match our resource or node in the body. + # + if @mucs[msg.from.strip].is_a?(MUC::MUCClient) and + msg.from != @mucs[msg.from.strip].jid and + msg.x("jabber:x:delay") == nil and + (msg.body =~ /\b#{Regexp.escape(@client.jid.resource)}\b/i or + msg.body =~ /\b#{Regexp.escape(@client.jid.node)}\b/i) + receive_normal_message(msg) + end end def check_alert_conditions(destination, conditions) @@ -264,8 +446,11 @@ module Mauve # Returns true if at least one of the presence specifiers for the jid # is met, false otherwise. Note that if the alerter can't see the alertee's # presence, only 'unknown' will match - generally, you'll want [:online, :unknown] - def check_jid_has_presence(jid, presence_or_presences) - return true if jid.match(/^muc:/) + def check_jid_has_presence(jid, presence_or_presences = [:online, :unknown]) + jid = JID.new(jid) unless jid.is_a?(JID) + + + return true if is_muc?(jid) reconnect unless @client @@ -291,15 +476,16 @@ module Mauve end results.include?(true) end - - end - # - # TODO parse message and ack as needed..? The trick is here to - # understand what the person sending the message wants. Could be - # difficult. - def receive_message(message) - @logger.debug "Received message from #{message.from}.. Ignoring for now." + def is_known_contact?(jid) + jid = JID.new(jid) unless jid.is_a?(JID) + + Configuration.current.people.any? do |username, person| + next unless person.xmpp.is_a?(JID) + person.xmpp.strip == jid.strip + end + end + end end end diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb index 2e4c737..23e0c1e 100644 --- a/lib/mauve/people_list.rb +++ b/lib/mauve/people_list.rb @@ -21,19 +21,21 @@ module Mauve alias username label def list - self[:list] + self[:list] || [] end # # Set up the logger def logger - @logger ||= Log4r::Logger.new self.class + @logger ||= Log4r::Logger.new self.class.to_s end # # Return the array of people # def people + logger.warn "No-one found in the people list for #{self.label}" if self.list.empty? + list.collect do |name| Configuration.current.people.has_key?(name) ? Configuration.current.people[name] : nil end.reject{|person| person.nil?} diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb index da3aa13..42b6baf 100644 --- a/lib/mauve/person.rb +++ b/lib/mauve/person.rb @@ -3,7 +3,7 @@ require 'timeout' require 'log4r' module Mauve - class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low) + class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low, :email, :xmpp, :sms) attr_reader :notification_thresholds @@ -24,26 +24,50 @@ module Mauve # class NotificationCaller - def initialize(alert, other_alerts, notification_methods, base_conditions={}) - logger = Log4r::Logger.new "mauve::NotificationCaller" + def initialize(person, alert, other_alerts, notification_methods, base_conditions={}) + @person = person @alert = alert @other_alerts = other_alerts @notification_methods = notification_methods @base_conditions = base_conditions end - def method_missing(name, destination, *args) - conditions = @base_conditions.merge(args[0] ? args[0] : {}) - notification_method = @notification_methods[name.to_s] + def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end + + # + # This method makes sure things liek + # + # xmpp + # works + # + def method_missing(name, *args) + # + # Work out the destination + # + if args.first.is_a?(String) + destination = args.pop + else + destination = @person.__send__(name) + end - unless notification_method - raise NoMethodError.new("#{name} not defined as a notification method") + if args.first.is_a?(Array) + conditions = @base_conditions.merge(args[0]) 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. # - notification_method.send_alert(destination, @alert, @other_alerts, conditions) + res = notification_method.send_alert(destination, @alert, @other_alerts, conditions) + # + # Log the result + logger.debug "Notification " + (res ? "succeeded" : "failed" ) + " for #{@person.username} using notifier '#{name}' to '#{destination}'" + + res end end @@ -174,6 +198,7 @@ module Mauve return if suppressed? or this_alert_suppressed result = NotificationCaller.new( + self, alert, current_alerts, Configuration.current.notification_methods, @@ -197,7 +222,7 @@ module Mauve # Returns the subset of current alerts that are relevant to this Person. # def current_alerts - Alert.all_current.select do |alert| + Alert.all_raised.select do |alert| my_last_update = AlertChanged.first(:person => username, :alert_id => alert.id) my_last_update && my_last_update.update_type != :cleared end diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb index 30536bb..ac24da4 100644 --- a/lib/mauve/server.rb +++ b/lib/mauve/server.rb @@ -12,7 +12,6 @@ require 'mauve/processor' require 'mauve/http_server' require 'log4r' - module Mauve class Server @@ -70,7 +69,7 @@ module Mauve # DataMapper.setup(:default, @config[:database]) - DataObjects::Sqlite3.logger = Log4r::Logger.new("Mauve::DataMapper") + # DataObjects::Sqlite3.logger = Log4r::Logger.new("Mauve::DataMapper") # # Update any tables. @@ -82,7 +81,7 @@ module Mauve # # Work out when the server was last stopped # - @stopped_at = self.last_heartbeat + # topped_at = self.last_heartbeat end def last_heartbeat @@ -91,7 +90,7 @@ module Mauve # [ Alert.last(:order => :updated_at.asc), AlertChanged.last(:order => :updated_at.asc) ]. - reject{|a| a.nil?}. + reject{|a| a.nil? or a.updated_at.nil? }. collect{|a| a.updated_at.to_time}. sort. last @@ -108,26 +107,61 @@ module Mauve def stop @stop = true + thread_list = Thread.list + + thread_list.delete(Thread.current) + THREAD_CLASSES.reverse.each do |klass| - klass.instance.stop + thread_list.delete(klass.instance) + klass.instance.stop unless klass.instance.nil? end - + + thread_list.each do |t| + t.exit + end + @logger.info("All threads stopped") end def run loop do + thread_list = Thread.list + + thread_list.delete(Thread.current) + THREAD_CLASSES.each do |klass| + thread_list.delete(klass.instance) + next if @frozen or @stop - + unless klass.instance.alive? - # ugh something has died. - klass.instance.join + # ugh something has died. + # + begin + klass.instance.join + rescue StandardError => ex + @logger.warn "Caught #{ex.to_s} whilst checking #{klass} thread" + @logger.debug ex.backtrace.join("\n") + end + # + # Start the stuff. klass.instance.start unless @stop end end + thread_list.each do |t| + next unless t.alive? + begin + t.join + rescue StandardError => ex + @logger.fatal "Caught #{ex.to_s} whilst checking threads" + @logger.debug ex.backtrace.join("\n") + self.stop + break + end + end + break if @stop sleep 1 diff --git a/lib/mauve/source_list.rb b/lib/mauve/source_list.rb index 4ffef15..23f5ae1 100644 --- a/lib/mauve/source_list.rb +++ b/lib/mauve/source_list.rb @@ -27,7 +27,7 @@ module Mauve ## Default contructor. def initialize () - @logger = Log4r::Logger.new "mauve::SourceList" + @logger = Log4r::Logger.new "Mauve::SourceList" @hash = Hash.new @http_head = Regexp.compile(/^http[s]?:\/\//) @http_tail = Regexp.compile(/\/.*$/) diff --git a/lib/mauve/timer.rb b/lib/mauve/timer.rb index 5355dcc..60b541c 100644 --- a/lib/mauve/timer.rb +++ b/lib/mauve/timer.rb @@ -21,7 +21,6 @@ module Mauve end def main_loop - @logger.debug "hello" # # Get the next alert. # @@ -73,7 +72,12 @@ module Mauve # # This is a rate-limiting step for alerts. # - Kernel.sleep 0.2 + Kernel.sleep 0.1 + # + # Not sure if this is needed or not. But the timer thread seems to + # freeze here, apparently stuck on a select() statement. + # + Thread.pass end return if self.should_stop? or next_to_notify.nil? diff --git a/lib/mauve/udp_server.rb b/lib/mauve/udp_server.rb index a570e8a..a873e77 100644 --- a/lib/mauve/udp_server.rb +++ b/lib/mauve/udp_server.rb @@ -68,11 +68,13 @@ module Mauve # # TODO: why is/isn't this non-block? # + i = 0 begin - # packet = @socket.recvfrom_nonblock(65535) - packet = @socket.recvfrom(65535) + packet = @socket.recvfrom_nonblock(65535) +# packet = @socket.recvfrom(65535) received_at = MauveTime.now - rescue Errno::EAGAIN, Errno::EWOULDBLOCK + rescue Errno::EAGAIN, Errno::EWOULDBLOCK => ex + puts "#{i += 1} + #{ex}" IO.select([@socket]) retry unless self.should_stop? end @@ -99,7 +101,7 @@ module Mauve # # Triggers loop to close socket. # - UDPSocket.open.send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.closed? + UDPSocket.open.send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.nil? or @socket.closed? super end diff --git a/lib/mauve/web_interface.rb b/lib/mauve/web_interface.rb index 4569cb6..210f88a 100644 --- a/lib/mauve/web_interface.rb +++ b/lib/mauve/web_interface.rb @@ -1,7 +1,11 @@ # encoding: UTF-8 +require 'haml' +require 'redcloth' + +require 'sinatra/tilt' require 'sinatra/base' require 'sinatra-partials' -require 'haml' + require 'rack' require 'rack-flash' @@ -16,15 +20,18 @@ module Mauve class PleaseAuthenticate < Exception; end - use Rack::Session::Cookie, :expire_after => 604800 # 7 days in seconds + use Rack::CommonLogger + use Rack::Chunked + use Rack::ContentLength + use Rack::Flash - enable :sessions +# Tilt.register :textile, RedClothTemplate - use Rack::Flash - - set :root, "/usr/share/mauve" - set :views, "#{root}/views" - set :public, "#{root}/static" + + # Ugh.. hacky way to dynamically configure the document root. + set :root, Proc.new{ HTTPServer.instance.document_root } + set :views, Proc.new{ root && File.join(root, 'views') } + set :public, Proc.new{ root && File.join(root, 'static') } set :static, true set :show_exceptions, true @@ -39,12 +46,43 @@ module Mauve ######################################################################## before do - @person = Configuration.current.people[session['username']] @title = "Mauve alert panel" + @person = nil + # + # Make sure we're authenticated. + # + + if session.has_key?('username') and Configuration.current.people.has_key?(session['username'].to_s) + # + # Phew, we're authenticated + # + @person = Configuration.current.people[session['username']] + + # + # A bit wasteful maybe..? + # + @alerts_raised = Alert.all_raised + @alerts_cleared = Alert.all_cleared + @alerts_ackd = Alert.all_acknowledged + @group_by = "subject" + else + # Uh-oh.. Intruder alert! + # + ok_urls = %w(/ /login /logout) + + unless ok_urls.include?(request.path_info) + flash['error'] = "You must be logged in to access that page." + redirect "/login?next_page=#{request.path_info}" + end + end end get '/' do - redirect '/alerts' + if @person.nil? + redirect '/login' + else + redirect '/alerts' + end end ######################################################################## @@ -53,36 +91,69 @@ module Mauve # # The password can be either the SSO or a local one defined # in the configuration file. - # + + get '/login' do + if @person + redirect '/' + else + @next_page = params[:next_page] || '/' + haml :login + end + end + post '/login' do usr = params['username'] pwd = params['password'] - ret_sso = helper_auth_SSO(usr, pwd) - ret_loc = helper_auth_local(usr, pwd) - if "success" == ret_sso or "success" == ret_loc + next_page = params['next_page'] + # + # Make sure we don't magically logout automatically :) + # + next_page = '/' if next_page == '/logout' + + if auth_helper(usr, pwd) session['username'] = usr + redirect next_page else - flash['error'] =<<__MSG -<hr /> <img src="/images/error.png" /> <br /> -ACCESS DENIED <br /> -#{ret_sso} <br /> -#{ret_loc} <hr /> -__MSG + flash['error'] = "Access denied." end - redirect '/alerts' end get '/logout' do session.delete('username') - redirect '/alerts' + redirect '/login' end get '/alerts' do - #now = MauveTime.now.to_f - please_authenticate() - find_active_alerts() - #pp MauveTime.now.to_f - now - haml(:alerts2) + redirect '/alerts/raised' + end + + get '/alerts/:alert_type' do + redirect "/alerts/#{params[:alert_type]}/subject" + end + + get '/alerts/:alert_type/:group_by' do + if %w(raised cleared acknowledged).include?(params[:alert_type]) + @alert_type = params[:alert_type] + else + @alert_type = "raised" + end + + if %w(subject source summary id alert_id level).include?(params[:group_by]) + @group_by = params[:group_by] + else + @group_by = "subject" + end + + case @alert_type + when "raised" + @grouped_alerts = group_by(@alerts_raised, @group_by) + when "cleared" + @grouped_alerts = group_by(@alerts_cleared, @group_by) + when "acknowledged" + @grouped_alerts = group_by(@alerts_ackd, @group_by) + end + + haml(:alerts) end get '/_alert_summary' do @@ -98,22 +169,20 @@ __MSG partial("head") end - get '/alert/:id/detail' do - please_authenticate - - content_type("text/html") # I think - Alert.get(params[:id]).detail + get '/alert/:id/_detail' do + content_type "text/html" + alert = Alert.get(params[:id]) + + haml :_detail, :locals => { :alert => alert } unless alert.nil? end get '/alert/:id' do - please_authenticate find_active_alerts @alert = Alert.get(params['id']) haml :alert end post '/alert/:id/acknowledge' do - please_authenticate alert = Alert.get(params[:id]) if alert.acknowledged? @@ -128,7 +197,6 @@ __MSG # Note that :until must be in seconds. post '/alert/acknowledge/:id/:until' do #now = MauveTime.now.to_f - please_authenticate alert = Alert.get(params[:id]) alert.acknowledge!(@person, params[:until].to_i()) @@ -140,7 +208,6 @@ __MSG post '/alert/:id/raise' do #now = MauveTime.now.to_f - please_authenticate alert = Alert.get(params[:id]) alert.raise! @@ -150,7 +217,6 @@ __MSG end post '/alert/:id/clear' do - please_authenticate alert = Alert.get(params[:id]) alert.clear! @@ -159,7 +225,6 @@ __MSG end post '/alert/:id/toggleDetailView' do - please_authenticate alert = Alert.get(params[:id]) if nil != alert @@ -171,8 +236,6 @@ __MSG end post '/alert/fold/:subject' do - please_authenticate - session[:display_folding][params[:subject]] = (true == session[:display_folding][params[:subject]])? false : true content_type("application/json") 'all is good'.to_json @@ -181,7 +244,6 @@ __MSG ######################################################################## get '/preferences' do - please_authenticate find_active_alerts haml :preferences end @@ -189,7 +251,6 @@ __MSG ######################################################################## get '/events' do - please_authenticate find_active_alerts find_recent_alerts haml :events @@ -199,11 +260,21 @@ __MSG helpers do include Sinatra::Partials - - def please_authenticate - raise PleaseAuthenticate.new unless @person + + def group_by(things, meth) + return {} if things.empty? + + raise ArgumentError.new "#{things.first.class} does not respond to #{meth}" unless things.first.respond_to?(meth) + + results = Hash.new{|h,k| h[k] = Array.new} + + things.each do |thing| + results[thing.__send__(meth)] << thing + end + + results end - + def find_active_alerts # FIXME: make sure alerts only appear once some better way @@ -287,25 +358,45 @@ __MSG ## Test for authentication with SSO. # - def helper_auth_SSO (usr, pwd) - auth = AuthSourceBytemark.new() - begin - return "success" if true == auth.authenticate(usr,pwd) - return "SSO did not regcognise your login/password combination." - rescue ArgumentError => ex - return "SSO argument error: #{ex.message}" - rescue => ex - return "SSO generic error: #{ex.message}" + def auth_helper (usr, pwd) + # First try Bytemark + # + auth = AuthBytemark.new() + result = begin + auth.authenticate(usr,pwd) + rescue Exception => ex + @logger.debug "Caught exception during Bytemark auth for #{usr} (#{ex.to_s})" + false end - end - ## Test for authentication with configuration file parameter. - # - def helper_auth_local (usr, pwd) - person = Configuration.current.people[params['username']] - return "I did not recognise your local login details." if !person - return "I did not recognise your local password." if Digest::SHA1.hexdigest(params['password']) != person.password - return "success" + if true == result + return true + else + @logger.debug "Bytemark authentication failed for #{usr}" + end + + # + # OK now try local auth + # + result = begin + if Configuration.current.people.has_key?(usr) + Digest::SHA1.hexdigest(params['password']) == Configuration.current.people[usr].password + end + rescue Exception => ex + @logger.debug "Caught exception during local auth for #{usr} (#{ex.to_s})" + false + end + + if true == result + return true + else + @logger.debug "Local authentication failed for #{usr}" + end + # + # Rate limit logins. + # + sleep 5 + false end end @@ -316,14 +407,13 @@ __MSG status 403 session[:display_alerts] = Hash.new() session[:display_folding] = Hash.new() - haml :please_authenticate end ######################################################################## # @see http://stackoverflow.com/questions/2239240/use-rackcommonlogger-in-sinatra def call(env) if true == @logger.nil? - @logger = Log4r::Logger.new("mauve::Rack") + @logger = Log4r::Logger.new("Mauve::Rack") end env['rack.errors'] = RackErrorsProxy.new(@logger) super(env) diff --git a/static/common b/static/common new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/static/common @@ -0,0 +1 @@ +.
\ No newline at end of file diff --git a/static/images/BytemarkLogo180.png b/static/images/BytemarkLogo180.png Binary files differnew file mode 100644 index 0000000..d3cb3c4 --- /dev/null +++ b/static/images/BytemarkLogo180.png diff --git a/static/javascript b/static/javascript new file mode 120000 index 0000000..e3b95b4 --- /dev/null +++ b/static/javascript @@ -0,0 +1 @@ +/usr/share/javascript
\ No newline at end of file diff --git a/static/mauve_utils.js b/static/mauve_utils.js index 2a14dc2..8a4a9db 100644 --- a/static/mauve_utils.js +++ b/static/mauve_utils.js @@ -1,150 +1,24 @@ -// rather simple first stab at automating image rollovers - any image with -// a class of auto_hover will set its source to be the original name + _hover.png -// when rolled over, and back again when the mouse moves away. -// -// need to initialise by calling addAutoHover() after document has loaded. -// -function addAutoHover() { - $$('img.auto_hover').each(function(image) { - image.observe('mouseover', function(event) { - image.src = image.src.gsub(".png", "_hover.png"); - }); - image.observe('mouseout', function(event) { - image.src = image.src.gsub("_hover.png", ".png"); - }); - preload = new Image(); - preload.src = image.src.gsub(".png", "_hover.png"); - }); -}; -function addRefresh() { - updater1 = new Ajax.PeriodicalUpdater("alert_summary", "/_alert_summary", - { method: 'get', frequency: 120 }); - updater2 = new Ajax.PeriodicalUpdater("alert_counts", "/_alert_counts", - { method: 'get', frequency: 120 }); -} +// Controls the showing of details on alerts. -// Pop up the big white box at the top when something goes wrong, scroll so -// user can see it. -// -function reportError(message) { - $('errors_list').insert('<li>'+message+'</li>'); - $('errors').show(); - $('errors').scrollTo(); +function next_date(n, d, when) { + switch(when) { + case "daytime" + next_daytime_hour(d) + n; + case "working" + next_working_hour(d) + n; + default + d + n; + } } -// Hide the big white box again -// -function clearErrors() { $('errors').hide(); } -// Wrapper around reportError to report an error in updating a particular -// alert. -// -function acknowledgeFailed(id, message) { - if (message) - reportError("<strong>Couldn't update alert "+id+":</strong> "+message); - else - reportError("<strong>Couldn't update alert "+id+"</strong>"); +function is_daytime_hour(d) { + return (d.getHours() => 8 and d.getHours() <= 17); } -// Updates the page from a JSON representation of a particular alert. -// -function updateAlert(alert) { - var strip = $('alert_'+alert.id); - if (!strip) { - reportError("Alert "+id+" not rendered - bug?"); - return; - } - - image = strip.down(".acknowledge img"); - image.src = alert.acknowledged_at ? - "/images/acknowledge_acknowledged.png" : - "/images/acknowledge_unacknowledged.png" - - if (strip.down(".source")) - strip.down(".source").update(alert.source); - if (strip.down(".subject")) - strip.down(".subject").update(alert.subject); - if (strip.down(".summary") && strip.down(".summary").down()) - strip.down(".summary").down().update(alert.summary); - strip.next().update(alert.detail); - - if (alert.acknowledged_at) - strip.next().hide(); -} -// called when user hits the acknowledge button for an alert - makes a callback -// to the server to communicate the change, and updates the button -// appropriately. -// -function toggleAcknowledge(id) { - updater = new Ajax.Request('/alert/'+id+'/acknowledge', { - - method: 'post', - - // ignored by server, see http://www.ruby-forum.com/topic/162976 for why - postBody: 'x', - - onFailure: function(xhr) { acknowledgeFailed(id, "Failure - "+xhr.statusText); }, - - onException: function(xhr, ex) { acknowledgeFailed(id, Dumper(ex)); }, - - onSuccess: function(xhr) { - if (xhr.status == 200) { - content_type = xhr.getResponseHeader("Content-Type"); - if (content_type != "application/json") { - acknowledgeFailed(id, "Got "+content_type+" not application/json from server"); - } else { - updateAlert(xhr.responseText.evalJSON()); - } - } else { - acknowledgeFailed(id, "Connection problem"); - } - } - }); -}; +function next_working_hour(d) { -// Controls the showing of details on alerts. -function toggleDetailView(id) { - updater = new Ajax.Request('/alert/'+id+'/toggleDetailView', { - method: 'post', - postBody: 'x', - onFailure: function(xhr) { acknowledgeFailed(id, "Failure - "+xhr.statusText); }, - onException: function(xhr, ex) { acknowledgeFailed(id, Dumper(ex)); }, - onSuccess: function(xhr) { - if (xhr.status == 200) { - content_type = xhr.getResponseHeader("Content-Type"); - if (content_type != "application/json") { - acknowledgeFailed(id, "Got "+content_type+" not application/json from server"); - } else { - //updateAlert(xhr.responseText.evalJSON()); - } - } else { - acknowledgeFailed(id, "Connection problem"); - } - } - }); } - -// Controls the showing of folding on alerts. -function toggleFoldingView(subject) { - updater = new Ajax.Request('/alert/fold/'+subject, { - method: 'post', - postBody: 'x', - onFailure: function(xhr) { acknowledgeFailed(subject, "Failure - "+xhr.statusText); }, - onException: function(xhr, ex) { acknowledgeFailed(subject, Dumper(ex)); }, - onSuccess: function(xhr) { - if (xhr.status == 200) { - content_type = xhr.getResponseHeader("Content-Type"); - if (content_type != "application/json") { - acknowledgeFailed(subject, "Got "+content_type+" not application/json from server"); - } else { - //updateAlert(xhr.responseText.evalJSON()); - } - } else { - acknowledgeFailed(subject, "Connection problem"); - } - } - }); -} diff --git a/static/mauve_utils.js.old b/static/mauve_utils.js.old new file mode 100644 index 0000000..2a14dc2 --- /dev/null +++ b/static/mauve_utils.js.old @@ -0,0 +1,150 @@ +// rather simple first stab at automating image rollovers - any image with +// a class of auto_hover will set its source to be the original name + _hover.png +// when rolled over, and back again when the mouse moves away. +// +// need to initialise by calling addAutoHover() after document has loaded. +// +function addAutoHover() { + $$('img.auto_hover').each(function(image) { + image.observe('mouseover', function(event) { + image.src = image.src.gsub(".png", "_hover.png"); + }); + image.observe('mouseout', function(event) { + image.src = image.src.gsub("_hover.png", ".png"); + }); + preload = new Image(); + preload.src = image.src.gsub(".png", "_hover.png"); + }); +}; + +function addRefresh() { + updater1 = new Ajax.PeriodicalUpdater("alert_summary", "/_alert_summary", + { method: 'get', frequency: 120 }); + updater2 = new Ajax.PeriodicalUpdater("alert_counts", "/_alert_counts", + { method: 'get', frequency: 120 }); +} + +// Pop up the big white box at the top when something goes wrong, scroll so +// user can see it. +// +function reportError(message) { + $('errors_list').insert('<li>'+message+'</li>'); + $('errors').show(); + $('errors').scrollTo(); +} +// Hide the big white box again +// +function clearErrors() { $('errors').hide(); } + +// Wrapper around reportError to report an error in updating a particular +// alert. +// +function acknowledgeFailed(id, message) { + if (message) + reportError("<strong>Couldn't update alert "+id+":</strong> "+message); + else + reportError("<strong>Couldn't update alert "+id+"</strong>"); +} + +// Updates the page from a JSON representation of a particular alert. +// +function updateAlert(alert) { + var strip = $('alert_'+alert.id); + if (!strip) { + reportError("Alert "+id+" not rendered - bug?"); + return; + } + + image = strip.down(".acknowledge img"); + image.src = alert.acknowledged_at ? + "/images/acknowledge_acknowledged.png" : + "/images/acknowledge_unacknowledged.png" + + if (strip.down(".source")) + strip.down(".source").update(alert.source); + if (strip.down(".subject")) + strip.down(".subject").update(alert.subject); + if (strip.down(".summary") && strip.down(".summary").down()) + strip.down(".summary").down().update(alert.summary); + strip.next().update(alert.detail); + + if (alert.acknowledged_at) + strip.next().hide(); +} + +// called when user hits the acknowledge button for an alert - makes a callback +// to the server to communicate the change, and updates the button +// appropriately. +// +function toggleAcknowledge(id) { + updater = new Ajax.Request('/alert/'+id+'/acknowledge', { + + method: 'post', + + // ignored by server, see http://www.ruby-forum.com/topic/162976 for why + postBody: 'x', + + onFailure: function(xhr) { acknowledgeFailed(id, "Failure - "+xhr.statusText); }, + + onException: function(xhr, ex) { acknowledgeFailed(id, Dumper(ex)); }, + + onSuccess: function(xhr) { + if (xhr.status == 200) { + content_type = xhr.getResponseHeader("Content-Type"); + if (content_type != "application/json") { + acknowledgeFailed(id, "Got "+content_type+" not application/json from server"); + } else { + updateAlert(xhr.responseText.evalJSON()); + } + } else { + acknowledgeFailed(id, "Connection problem"); + } + } + }); +}; + + +// Controls the showing of details on alerts. +function toggleDetailView(id) { + updater = new Ajax.Request('/alert/'+id+'/toggleDetailView', { + method: 'post', + postBody: 'x', + onFailure: function(xhr) { acknowledgeFailed(id, "Failure - "+xhr.statusText); }, + onException: function(xhr, ex) { acknowledgeFailed(id, Dumper(ex)); }, + onSuccess: function(xhr) { + if (xhr.status == 200) { + content_type = xhr.getResponseHeader("Content-Type"); + if (content_type != "application/json") { + acknowledgeFailed(id, "Got "+content_type+" not application/json from server"); + } else { + //updateAlert(xhr.responseText.evalJSON()); + } + } else { + acknowledgeFailed(id, "Connection problem"); + } + } + }); +} + + +// Controls the showing of folding on alerts. +function toggleFoldingView(subject) { + updater = new Ajax.Request('/alert/fold/'+subject, { + method: 'post', + postBody: 'x', + onFailure: function(xhr) { acknowledgeFailed(subject, "Failure - "+xhr.statusText); }, + onException: function(xhr, ex) { acknowledgeFailed(subject, Dumper(ex)); }, + onSuccess: function(xhr) { + if (xhr.status == 200) { + content_type = xhr.getResponseHeader("Content-Type"); + if (content_type != "application/json") { + acknowledgeFailed(subject, "Got "+content_type+" not application/json from server"); + } else { + //updateAlert(xhr.responseText.evalJSON()); + } + } else { + acknowledgeFailed(subject, "Connection problem"); + } + } + }); +} diff --git a/static/stylesheets/bytemark.css b/static/stylesheets/bytemark.css new file mode 100644 index 0000000..c0fd54a --- /dev/null +++ b/static/stylesheets/bytemark.css @@ -0,0 +1,183 @@ +/*----------------------------\ +| Bytemark Internal Stylesheet| +\-----------------------------/ + +To give your page a nice feel, make sure it starts: + <html><head> + <link rel="stylesheet" + href="https://admin.bytemark.co.uk/common/bytemark.css" + type="text/css" + /> + </head><body> + . + . + . + . + </body></head> + +Using headers in the correct order (i.e. h1, h2, h3) also helps. + +Patrick +2004-06-38 +*/ + +pre, code { + font-family: monospace; +} + +ul { + background-color: white; + color: black; +} + +.OK { + background-color: LawnGreen; + color: black; +} + +.Failed { + background-color: OrangeRed; + color: black; +} + +h1 { + background-color: #ffea05; + color: black; + padding: 10px; + margin: 5px; + font-size: x-large; + font-weight: normal; +} +h2 { + background-color: #ABABAB; + color: white; + padding: 5px 5px; + margin: 5px 5px 0px 5px; + font-weight: bold; + font-size: large; +} +h2 a { + color: white; + font-weight: bold; + font-size: large; +} + +h3, th { + font-weight: bold; + font-size: small; + background-color: #E4E4E4; + color: black; +} +h3 { + padding: 3px 5px; + margin: 5px 5px 0px 5px; +} +h4 { + padding: 3px 5px; + margin: 5px 5px 0px 5px; +} +html { + background-color: #666; + color: white; + padding: 5px 5px; + font-family: arial, sans-serif; +} + +body { + margin: 0px; + padding: 5px 5px 5px 5px; + background-color: white; + color: black; + border: none; +} + +div#navbar { + /* try to cancel out the body padding above, leaving a bit of a margin beneath */ + margin: -5px -5px 5px -5px; + padding-left: 15px; + width: 100%; + background-color: #666; + font-size: 14px; +} + +div#navbar ul { + margin: 0px; + padding: 0px; + list-style: none; +} + +div#navbar ul li { + float: left; + margin-right: 2px; + padding: 4px 4px; + background: none; + color: #333; + background: #ccc; + border: 1px solid #9b8748; + border-bottom: none; +} + +div#navbar a { + display: block; + text-decoration: none; + margin: 0px; + min-height: 16px; + color: inherit; + vertical-align: bottom; +} + +div#navbar li#nav_index a { + background-image: url(/common/images/BytemarkLogo180.png); + background-repeat:no-repeat; + background-position:left center; + padding-left: 180px; + width: 0px; + overflow: hidden; +} + +div#navbar li#nav_index, div#navbar li#nav_selected { + background-color: white; + color: #333; +} + +div#navbar br { + clear: both; +} + +p { + margin: 0px 5px; + padding: 7px; + background-color: white; + color: black; +} +table { + margin: 5px 0px; + padding: 0px; +} +table.full { + width: 100%; +} +tr { + margin: 0px; + padding: 0px; +} +td { + vertical-align: top; + margin: 5px; + padding: 5px; +} +ul { + padding: 0px 20px; +} +a { + text-decoration: none; +} +a:link { + color: #0061ab; +} + +div#nav form { + display: inline; +} + + diff --git a/static/stylesheets/mauve.css b/static/stylesheets/mauve.css new file mode 100644 index 0000000..d15e588 --- /dev/null +++ b/static/stylesheets/mauve.css @@ -0,0 +1,41 @@ +tr.hilight { + background-color: #eee; +} + +tr.triggered.hilight { + background-color: #f98; +} + +tr.acknowledged.triggered.hilight { + background-color: #fa5; +} + + +.triggered { + background-color: #fba; +} + +.acknowledged { + color: #444; +} + +.acknowledged.triggered { + background-color: #fb6; +} + +.missing_data { + +} + +body { + min-width: 800px; +} + +.out_of_date { + font-style: italic; +} + +.detail { + background-color: white +} + diff --git a/views/_detail.haml b/views/_detail.haml new file mode 100644 index 0000000..282e454 --- /dev/null +++ b/views/_detail.haml @@ -0,0 +1,3 @@ +.detail + :textile + #{alert.detail} diff --git a/views/_header.haml b/views/_header.haml new file mode 100644 index 0000000..4f5c12a --- /dev/null +++ b/views/_header.haml @@ -0,0 +1,9 @@ +!!! XML +!!! +%html + %head + %meta{:name=>"viewport", :content=>"width=device-width"}/ + %title #{@title}: Authentication required + %link{:rel => "stylesheet", :href => "/stylesheets/bytemark.css"}/ + %link{:rel => "stylesheet", :href => "/stylesheets/mauve.css"}/ + %script{:src => 'javascript/jquery/jquery.js', :type => 'text/javascript'} diff --git a/views/_navbar.haml b/views/_navbar.haml new file mode 100644 index 0000000..2384baf --- /dev/null +++ b/views/_navbar.haml @@ -0,0 +1,15 @@ +#navbar + %ul + %li + %a{:href => "/"} Mauve + - if @person + %li{:id => [ @alert_type == "raised" && "nav_selected"]} + %a{:href => '/alerts/raised/'+@group_by} Raised (#{@alerts_raised.length}) + %li{:id => [ @alert_type == "acknowledged" && "nav_selected"]} + %a{:href => '/alerts/acknowledged/'+@group_by} Ack'd (#{@alerts_ackd.length}) + %li{:id => [ @alert_type == "cleared" && "nav_selected"]} + %a{:href => '/alerts/cleared/'+@group_by} Cleared (#{@alerts_cleared.length}) + %li + %a{:href => '/logout'} Log out + %br + diff --git a/views/_navigation.haml b/views/_navigation.haml index 6c47b28..c3301b2 100644 --- a/views/_navigation.haml +++ b/views/_navigation.haml @@ -5,7 +5,7 @@ %a{:href => "/logout"} Logout <strong>(#{@person.username})</strong> - else %form{:action => '/login', :method => :POST} - Username + UsernameArse %input{:name => 'username', :type => 'text', :size => 10} Password %input{:name => 'password', :type => 'password', :size => 10} diff --git a/views/alert.haml b/views/alert.haml index 4310ddc..9d25c47 100644 --- a/views/alert.haml +++ b/views/alert.haml @@ -2,18 +2,18 @@ %html %head %title #{@title}: Alert #{@alert.id}: #{@alert.get_safe_html_summary} - %link{:rel => "stylesheet", :href => "/alerts2.css"} - %link{:rel => "stylesheet", :href => "/alerts-mobil.css", :media => "handheld"} - %script{:type => "text/javascript", :src => "/prototype.js"} - %script{:type => "text/javascript", :src => "/mauve_utils.js"} - %script{:type => "text/javascript", :src => "/datadumper.js"} + %link{:rel => "stylesheet", :href => "/alerts2.css"}/ + %link{:rel => "stylesheet", :href => "/alerts-mobil.css", :media => "handheld"} / + %script{:type => "text/javascript", :src => "/prototype.js"}/ + %script{:type => "text/javascript", :src => "/mauve_utils.js"}/ + %script{:type => "text/javascript", :src => "/datadumper.js"}/ %body{:onLoad => "addAutoHover(); addRefresh();"} .head= partial("head3") #about_alert %h1.summary #{@alert.get_safe_html_subject}: #{@alert.get_safe_html_summary} %h2.details Alert Details - if @alert.detail - .detail= @alert.get_details + .arse= partial("detail") - if @alert.source != @alert.subject %h2.source Source .source= @alert.get_safe_html_source diff --git a/views/alerts.haml b/views/alerts.haml index af65dea..e2a8c82 100644 --- a/views/alerts.haml +++ b/views/alerts.haml @@ -1,24 +1,64 @@ -!!! +!!! HTML5 %html %head - %title #{@title}: Current alerts (logged in as #{@person.username}) - %link{:rel => "stylesheet", :href => "/alerts.css"} - %link{:rel => "stylesheet", :href => "/alerts-mobil.css", :media => "handheld"} - %script{:type => "text/javascript", :src => "/prototype.js"} - %script{:type => "text/javascript", :src => "/mauve_utils.js"} - %script{:type => "text/javascript", :src => "/datadumper.js"} - %body{:onLoad => "addAutoHover(); addRefresh();"} - #header - %h1= @title - #errors{:style => "display: none;"} - %h1 - Trouble! - ( - %a{:href=>"#", :onClick => "clearErrors();"} hide - ) - %ul#errors_list - %p Either the alert server or your internet connection is malfunctioning, so you may want to try refreshing the page. - #alert_counts= partial("alert_counts") - #navigation= partial("navigation") - #alert_summary= partial("alert_summary") - //%p The session is #{session.inspect()} + %meta{:name=>"viewport", :content=>"width=device-width"}/ + %title #{@title}: Alerts + %link{:rel => "stylesheet", :href => "/stylesheets/bytemark.css"}/ + %link{:rel => "stylesheet", :href => "/stylesheets/mauve.css"}/ + %script{:src => '/javascript/prototype/prototype.js', :type => 'text/javascript'} + %script{:src => '/javascript/scriptaculous/scriptaculous.js', :type => 'text/javascript'} + %body + = partial('navbar') + %h1 Mauve Alerts + %form + %table + %tr + %th + %input{ :type => "checkbox", :name => "all" } + %th Lvl + %th Subject + %th Summary + %th Raised at + %th Actions + - count = 0 + - @grouped_alerts.each do |group, alerts| + - count += 1 + - row_class = [ count % 2 == 0 && "hilight", alerts.first.raised? && "triggered", alerts.first.acknowledged? && "acknowledged"] + - first_n = (alerts.length > 2 ? 1 : 2) + - alerts.first(first_n).each do |alert| + %tr{ :class => row_class, :id => ["summary", alert.id] } + %td + %input{ :type => "checkbox", :name => alert.id } + %td #{alert.level} + %td #{alert.subject} + %td + #{alert.summary} + %a{ :href => "/alert/#{alert.id}", :onclick => "$('detail_#{alert.id}').toggle(); return false;"} show details ↓ + %td #{alert.raised_at} + %td arse + %tr{ :class => %w(detail) + row_class, :id => ["detail", alert.id], :style => "display: none;" } + %td + %td{:colspan => 5} + = partial(:detail, :locals => {:alert => alert}) + %p + Source: #{alert.source} • + %a{ :href => "/alert/#{alert.id}" } More details + • + %a{ :href => "#", :onclick => "$('detail_#{alert.id}').hide(); return false;"} Hide details ↑ + - if alerts.length > 2 + %tr{ :class => [ count % 2 == 0 && "hilight", alerts.first.raised? && "triggered", alerts.first.acknowledged? && "acknowledged"]} + %td + %td{:colspan => 5} There are #{alerts.length - 1} more alerts in this group. + %tr + %td{:colspan => 6} + Acknowledge these alerts for + %input{ :name => 'hours', :type => "number", :min => 1, :max => 24, :value => 2} + %select + %option{ :value => "daytime" } daytime + %option{ :value => "working" } working + %option{ :value => "wall" } wall + hours (until + %span{:id => "ack_until"} + = Time.now + 2 + ) + %input{ :type => 'submit' } diff --git a/views/login.haml b/views/login.haml new file mode 100644 index 0000000..225c2e8 --- /dev/null +++ b/views/login.haml @@ -0,0 +1,27 @@ +!!! XML +!!! +%html + %head + %meta{:name=>"viewport", :content=>"width=device-width"}/ + %title #{@title}: Authentication required + %link{:rel => "stylesheet", :href => "/stylesheets/bytemark.css"}/ + %link{:rel => "stylesheet", :href => "/stylesheets/mauve.css"}/ + %script{:src => 'javascript/jquery/jquery.js', :type => 'text/javascript'} + %body + = partial('navbar') + %h1 Mauve Alerts + - if flash['error'] + %p.error= flash['error'] + %form{:action => '/login', :method => :POST} + %fieldset + %legend Please log in + %label{:for => "username"} Username + %input{:name => 'username', :type => 'text'}/ + %br + %label{:for => "password", :title => "This is either your Single Sign On password or a PIN."} Password / PIN + %input{:name => 'password', :type => 'password'}/ + %br + %input{:type => 'hidden', :name => 'next_page', :value => @next_page}/ + %input{:type => 'submit', :value => 'Log in'}/ + + diff --git a/views/please_authenticate.haml b/views/please_authenticate.haml deleted file mode 100644 index 9416c8e..0000000 --- a/views/please_authenticate.haml +++ /dev/null @@ -1,25 +0,0 @@ -!!! -%html - %head - %meta{:name=>"viewport", :content=>"width=device-width"} - %title #{@title}: Authentication required - %link{:rel => "stylesheet", :href => "/alerts2.css"} - %body - .loginForm - %form{:action => '/login', :method => :POST} - Username - %input{:name => 'username', :type => 'text'} - %br - Password - %input{:name => 'password', :type => 'password'} - %br - %input{:type => 'submit', :value => 'Log into the Mauve alert panel', :class=>"submitLoginButton"} - .loginNotes - This is either your single sign on or a PIN. - %br - You - %strong - must - be logged in to view alerts. - - |