lib/flapjack/data/notification_rule.rb in flapjack-0.7.2 vs lib/flapjack/data/notification_rule.rb in flapjack-0.7.3

- old
+ new

@@ -27,146 +27,250 @@ logger = options[:logger] # sanity check return unless redis.exists("notification_rule:#{rule_id}") - rule = self.new({:id => rule_id}, {:redis => redis}) - rule.refresh - rule + self.new({:id => rule_id.to_s}, {:redis => redis}) end # replacing save! etc - def self.add(rule_data, options) + def self.add(rule_data, options = {}) raise "Redis connection not set" unless redis = options[:redis] rule_id = SecureRandom.uuid self.add_or_update(rule_data.merge(:id => rule_id), :redis => redis) self.find_by_id(rule_id, :redis => redis) end - # add user's timezone string to the hash, deserialise - # time in the user's timezone also - def self.time_restriction_to_ice_cube_hash(tr, time_zone) - tr = symbolize(tr) - - tr[:start_date] = tr[:start_time].dup - tr.delete(:start_time) - - if tr[:start_date].is_a?(String) - tr[:start_date] = { :time => tr[:start_date] } - end - if tr[:start_date].is_a?(Hash) - tr[:start_date][:time] = time_zone.parse(tr[:start_date][:time]) - tr[:start_date][:zone] = time_zone.name - end - - if tr[:end_time].is_a?(String) - tr[:end_time] = { :time => tr[:end_time] } - end - if tr[:end_time].is_a?(Hash) - tr[:end_time][:time] = time_zone.parse(tr[:end_time][:time]) - tr[:end_time][:zone] = time_zone.name - end - - # rewrite Weekly to IceCube::WeeklyRule, etc - tr[:rrules].each {|rrule| - rrule[:rule_type] = "IceCube::#{rrule[:rule_type]}Rule" - } - - tr + def update(rule_data) + return false unless self.class.add_or_update(rule_data.merge(:id => @id), + :redis => @redis) + refresh + true end - def self.time_restriction_from_ice_cube_hash(tr, time_zone) - tr[:start_date] = time_zone.utc_to_local(tr[:start_date][:time]).strftime "%Y-%m-%d %H:%M:%S" - tr[:end_time] = time_zone.utc_to_local(tr[:end_time][:time]).strftime "%Y-%m-%d %H:%M:%S" + # NB: ice_cube doesn't have much rule data validation, and has + # problems with infinite loops if the data can't logically match; see + # https://github.com/seejohnrun/ice_cube/issues/127 & + # https://github.com/seejohnrun/ice_cube/issues/137 + # We may want to consider some sort of timeout-based check around + # anything that could fall into that. + # + # We don't want to replicate IceCube's from_hash behaviour here, + # but we do need to apply some sanity checking on the passed data. + def self.time_restriction_to_icecube_schedule(tr, timezone) + return unless !tr.nil? && tr.is_a?(Hash) + return if timezone.nil? && !timezone.is_a?(ActiveSupport::TimeZone) + return unless tr = prepare_time_restriction(tr, timezone) - # rewrite IceCube::WeeklyRule to Weekly, etc - tr[:rrules].each {|rrule| - rrule[:rule_type] = /^.*\:\:(.*)Rule$/.match(rrule[:rule_type])[1] - } - - tr[:start_time] = tr[:start_date].dup - tr.delete(:start_date) - - tr + IceCube::Schedule.from_hash(tr) end - def refresh - rule_data = @redis.hgetall("notification_rule:#{@id}") - - @contact_id = rule_data['contact_id'] - @entity_tags = Yajl::Parser.parse(rule_data['entity_tags'] || '') - @entities = Yajl::Parser.parse(rule_data['entities'] || '') - @time_restrictions = Yajl::Parser.parse(rule_data['time_restrictions'] || '') - @warning_media = Yajl::Parser.parse(rule_data['warning_media'] || '') - @critical_media = Yajl::Parser.parse(rule_data['critical_media'] || '') - @warning_blackhole = ((rule_data['warning_blackhole'] || 'false').downcase == 'true') - @critical_blackhole = ((rule_data['critical_blackhole'] || 'false').downcase == 'true') - - end - - def update(rule_data) - self.class.add_or_update(rule_data.merge(:id => @id), :redis => @redis) - self.refresh - end - def to_json(*args) - hash = (Hash[ *([:id, :contact_id, :entity_tags, :entities, - :time_restrictions, :warning_media, :critical_media, - :warning_blackhole, :critical_blackhole].collect {|k| - [k, self.send(k)] - }).flatten(1) ]) - hash.to_json + self.class.hashify(:id, :contact_id, :entity_tags, :entities, + :time_restrictions, :warning_media, :critical_media, + :warning_blackhole, :critical_blackhole) {|k| + [k, self.send(k)] + }.to_json end # tags or entity names match? # nil @entity_tags and nil @entities matches def match_entity?(event) - return true if (@entity_tags.nil? or @entity_tags.empty?) and - (@entities.nil? or @entities.empty?) - return true if @entities.include?(event.split(':').first) # TODO: return true if event's entity tags match entity tag list on the rule - return false + ((@entity_tags.nil? || @entity_tags.empty?) && (@entities.nil? || @entities.empty?)) || + (@entities.include?(event.split(':').first)) end def blackhole?(severity) - return true if 'warning'.eql?(severity.downcase) and @warning_blackhole - return true if 'critical'.eql?(severity.downcase) and @critical_blackhole - return false + ('warning'.eql?(severity.downcase) && @warning_blackhole) || + ('critical'.eql?(severity.downcase) && @critical_blackhole) end def media_for_severity(severity) case severity when 'warning' - media_list = @warning_media + @warning_media when 'critical' - media_list = @critical_media + @critical_media end - media_list end private def initialize(rule_data, opts = {}) @redis ||= opts[:redis] - @logger = opts[:logger] raise "a redis connection must be supplied" unless @redis - @id = rule_data[:id] + @logger = opts[:logger] + @id = rule_data[:id] + refresh end def self.add_or_update(rule_data, options = {}) - raise ":id is a required key in rule_data" unless rule_data[:id] - redis = options[:redis] + raise "a redis connection must be supplied" unless redis - rule_data[:entities] = Yajl::Encoder.encode(rule_data[:entities]) - rule_data[:entity_tags] = Yajl::Encoder.encode(rule_data[:entity_tags]) - rule_data[:time_restrictions] = Yajl::Encoder.encode(rule_data[:time_restrictions]) - rule_data[:warning_media] = Yajl::Encoder.encode(rule_data[:warning_media]) - rule_data[:critical_media] = Yajl::Encoder.encode(rule_data[:critical_media]) + return unless self.validate_data(rule_data, options) - redis.sadd("contact_notification_rules:#{rule_data[:contact_id]}", rule_data[:id]) - redis.hmset("notification_rule:#{rule_data[:id]}", *rule_data.flatten) + # whitelisting fields, rather than passing through submitted data directly + json_rule_data = { + :id => rule_data[:id].to_s, + :contact_id => rule_data[:contact_id].to_s, + :entities => Yajl::Encoder.encode(rule_data[:entities]), + :entity_tags => Yajl::Encoder.encode(rule_data[:entity_tags]), + :time_restrictions => Yajl::Encoder.encode(rule_data[:time_restrictions]), + :warning_media => Yajl::Encoder.encode(rule_data[:warning_media]), + :critical_media => Yajl::Encoder.encode(rule_data[:critical_media]), + :warning_blackhole => rule_data[:warning_blackhole], + :critical_blackhole => rule_data[:critical_blackhole], + } + + redis.sadd("contact_notification_rules:#{json_rule_data[:contact_id]}", + json_rule_data[:id]) + redis.hmset("notification_rule:#{json_rule_data[:id]}", + *json_rule_data.flatten) + true + end + + def self.prepare_time_restriction(time_restriction, timezone = nil) + # this will hand back a 'deep' copy + tr = symbolize(time_restriction) + + return unless tr.has_key?(:start_time) && tr.has_key?(:end_time) + + parsed_time = proc {|t| + if t.is_a?(Time) + t + else + begin; (timezone || Time).parse(t); rescue ArgumentError; nil; end + end + } + + start_time = case tr[:start_time] + when String, Time + parsed_time.call(tr.delete(:start_time).dup) + when Hash + time_hash = tr.delete(:start_time).dup + parsed_time.call(time_hash[:time]) + end + + end_time = case tr[:end_time] + when String, Time + parsed_time.call(tr.delete(:end_time).dup) + when Hash + time_hash = tr.delete(:end_time).dup + parsed_time.call(time_hash[:time]) + end + + return unless start_time && end_time + + tr[:start_date] = timezone ? + {:time => start_time, :zone => timezone.name} : + start_time + + tr[:end_date] = timezone ? + {:time => end_time, :zone => timezone.name} : + end_time + + tr[:duration] = end_time - start_time + + # check that rrule types are valid IceCube rule types + return unless tr[:rrules].is_a?(Array) && + tr[:rrules].all? {|rr| rr.is_a?(Hash)} && + (tr[:rrules].map {|rr| rr[:rule_type]} - + ['Daily', 'Hourly', 'Minutely', 'Monthly', 'Secondly', + 'Weekly', 'Yearly']).empty? + + # rewrite Weekly to IceCube::WeeklyRule, etc + tr[:rrules].each {|rrule| + rrule[:rule_type] = "IceCube::#{rrule[:rule_type]}Rule" + } + + # TODO does this need to check classes for the following values? + # "validations": { + # "day": [1,2,3,4,5] + # }, + # "interval": 1, + # "week_start": 0 + + tr + end + + def self.validate_data(d, options = {}) + # hash with validation => error_message + validations = {proc { d.has_key?(:id) } => + "id not set", + + proc { d.has_key?(:entities) && + d[:entities].is_a?(Array) && + d[:entities].all? {|e| e.is_a?(String)} } => + "entities must be a list of strings", + + proc { d.has_key?(:entity_tags) && + d[:entity_tags].is_a?(Array) && + d[:entity_tags].all? {|et| et.is_a?(String)}} => + "entity_tags must be a list of strings", + + proc { (d.has_key?(:entities) && + d[:entities].is_a?(Array) && + (d[:entities].size > 0)) || + (d.has_key?(:entity_tags) && + d[:entity_tags].is_a?(Array) && + (d[:entity_tags].size > 0)) } => + "entities or entity tags must have at least one value", + + proc { d.has_key?(:time_restrictions) && + d[:time_restrictions].all? {|tr| + !!prepare_time_restriction(symbolize(tr)) + } + } => + "time restrictions are invalid", + + # TODO should the media types be checked against a whitelist? + proc { d.has_key?(:warning_media) && + d[:warning_media].is_a?(Array) && + d[:warning_media].all? {|et| et.is_a?(String)}} => + "warning_media must be a list of strings", + + proc { d.has_key?(:critical_media) && + d[:critical_media].is_a?(Array) && + d[:critical_media].all? {|et| et.is_a?(String)}} => + "critical_media must be a list of strings", + + proc { d.has_key?(:warning_blackhole) && + [TrueClass, FalseClass].include?(d[:warning_blackhole].class) } => + "warning_blackhole must be true or false", + + proc { d.has_key?(:critical_blackhole) && + [TrueClass, FalseClass].include?(d[:critical_blackhole].class) } => + "critical_blackhole must be true or false", + } + + errors = validations.keys.inject([]) {|ret,vk| + ret << "Rule #{validations[vk]}" unless vk.call + ret + } + + return true if errors.empty? + + if logger = options[:logger] + error_str = errors.join(", ") + logger.info "validation error: #{error_str}" + end + false + end + + def refresh + rule_data = @redis.hgetall("notification_rule:#{@id}") + + @contact_id = rule_data['contact_id'] + @entity_tags = Yajl::Parser.parse(rule_data['entity_tags'] || '') + @entities = Yajl::Parser.parse(rule_data['entities'] || '') + @time_restrictions = Yajl::Parser.parse(rule_data['time_restrictions'] || '') + @warning_media = Yajl::Parser.parse(rule_data['warning_media'] || '') + @critical_media = Yajl::Parser.parse(rule_data['critical_media'] || '') + @warning_blackhole = ((rule_data['warning_blackhole'] || 'false').downcase == 'true') + @critical_blackhole = ((rule_data['critical_blackhole'] || 'false').downcase == 'true') end end end end