lib/nagiosharder.rb in nagiosharder-0.5.0.rc1 vs lib/nagiosharder.rb in nagiosharder-0.5.0

- old
+ new

@@ -1,11 +1,14 @@ +# encoding: UTF-8 + require 'restclient' require 'nokogiri' require 'active_support' # fine, we'll just do all of activesupport instead of the parts I want. thank Rails 3 for shuffling requires around. require 'cgi' require 'hashie' require 'nagiosharder/filters' +require 'nagiosharder/commands' # :( require 'active_support/version' # double and triplely ensure ActiveSupport::VERSION is around if ActiveSupport::VERSION::MAJOR > 2 require 'active_support/core_ext/array' @@ -23,175 +26,198 @@ class NagiosHarder class Site attr_accessor :nagios_url, :user, :password, :default_options, :default_cookies, :version, :nagios_time_format include HTTParty::ClassMethods - def initialize(nagios_url, user, password, version = 3, nagios_time_format = nil) + def initialize(nagios_url, user, password, version = 3, nagios_time_format = nil, ssl_verify = true) @nagios_url = nagios_url.gsub(/\/$/, '') @user = user @password = password - @default_options = {} + @default_options = {:verify => ssl_verify} @default_cookies = {} @version = version debug_output if ENV['DEBUG'] basic_auth(@user, @password) if @user && @password - @nagios_time_format = if nagios_time_format == 'us' - "%m-%d-%Y %H:%M:%S" - else - if @version.to_i < 3 - "%m-%d-%Y %H:%M:%S" - else - "%Y-%m-%d %H:%M:%S" - end - end + @nagios_time_format = case nagios_time_format + when 'us' + "%m-%d-%Y %H:%M:%S" + when 'euro' + "%d-%m-%Y %H:%M:%S" + when 'iso8601' + "%Y-%m-%d %H:%M:%S" + else + if @version.to_i == 3 # allows compatability with nagios 4 + "%Y-%m-%dT%H:%M:%S" + else + "%m-%d-%Y %H:%M:%S" + end + end self end - def acknowledge_service(host, service, comment) + def post_command(body) + # cmd_mod is always CMDMODE_COMMIT + body = {:cmd_mod => 2}.merge(body) + response = post(cmd_url, :body => body) + response.code == 200 && response.body.match(/successful/) && true + end + + def acknowledge_host(host, comment) request = { - :cmd_typ => 34, - :cmd_mod => 2, + :cmd_typ => COMMANDS[:acknowledge_host_problem], :com_author => @user, :com_data => comment, :host => host, - :service => service, :send_notification => true, :persistent => false, :sticky_ack => true } - response = post(cmd_url, :body => request) - response.code == 200 && response.body =~ /successful/ + post_command(request) end - def acknowledge_host(host, comment) + def unacknowledge_host(host) request = { - :cmd_typ => 33, - :cmd_mod => 2, + :cmd_typ => COMMANDS[:remove_host_acknowledgement], + :host => host + } + + post_command(request) + end + + def acknowledge_service(host, service, comment) + request = { + :cmd_typ => COMMANDS[:acknowledge_service_problem], :com_author => @user, :com_data => comment, :host => host, + :service => service, :send_notification => true, :persistent => false, :sticky_ack => true } - response = post(cmd_url, :body => request) - response.code == 200 && response.body =~ /successful/ + post_command(request) end - + def unacknowledge_service(host, service) request = { - :cmd_typ => 52, - :cmd_mod => 2, + :cmd_typ => COMMANDS[:remove_service_acknowledgement], :host => host, :service => service } - response = post(cmd_url, :body => request) - response.code == 200 && response.body =~ /successful/ + post_command(request) end - def schedule_service_downtime(host, service, options = {}) + def schedule_host_downtime(host, options = {}) + options[:type] ||= :fixed + request = { - :cmd_mod => 2, - :cmd_typ => 56, + :cmd_typ => COMMANDS[:schedule_host_downtime], :com_author => options[:author] || "#{@user} via nagiosharder", :com_data => options[:comment] || 'scheduled downtime by nagiosharder', :host => host, - :service => service, + :childoptions => 0, :trigger => 0 } + # FIXME we could use some option checking... + request[:fixed] = case options[:type].to_sym when :fixed then 1 when :flexible then 0 - else 1 + else 1 # default to fixed end - - if request[:fixed] == 0 + if request[:fixed] == 0 request[:hours] = options[:hours] request[:minutes] = options[:minutes] end - request[:start_time] = formatted_time_for(options[:start_time]) - request[:end_time] = formatted_time_for(options[:end_time]) + request[:start_time] = formatted_time_for(options[:start_time] || Time.now) + request[:end_time] = formatted_time_for(options[:end_time] || Time.now + 1.hour) - response = post(cmd_url, :body => request) - - response.code == 200 && response.body =~ /successful/ + post_command(request) end + + def schedule_service_downtime(host, service, options = {}) + options[:type] ||= :fixed - def schedule_host_downtime(host, options = {}) request = { - :cmd_mod => 2, - :cmd_typ => 55, + :cmd_typ => COMMANDS[:schedule_service_downtime], :com_author => options[:author] || "#{@user} via nagiosharder", :com_data => options[:comment] || 'scheduled downtime by nagiosharder', :host => host, - :childoptions => 0, + :service => service, :trigger => 0 } - # FIXME we could use some option checking... - request[:fixed] = case options[:type].to_sym when :fixed then 1 when :flexible then 0 - else 1 # default to fixed + else 1 end - if request[:fixed] == 0 + + if request[:fixed] == 0 request[:hours] = options[:hours] request[:minutes] = options[:minutes] end - request[:start_time] = formatted_time_for(options[:start_time]) - request[:end_time] = formatted_time_for(options[:end_time]) + request[:start_time] = formatted_time_for(options[:start_time] || Time.now) + request[:end_time] = formatted_time_for(options[:end_time] || Time.now + 1.hour) - response = post(cmd_url, :body => request) - - response.code == 200 && response.body =~ /successful/ + post_command(request) end def cancel_downtime(downtime_id, downtime_type = :host_downtime) - downtime_types = { - :host_downtime => 78, - :service_downtime => 79 + request = { + :cmd_typ => COMMANDS["del_#{downtime_type}".to_sym], + :down_id => downtime_id } - response = post(cmd_url, :body => { - :cmd_typ => downtime_types[downtime_type], - :cmd_mod => 2, - :down_id => downtime_id - }) - response.code == 200 && response.body =~ /successful/ + + post_command(request) end def schedule_host_check(host) - response = post(cmd_url, :body => { - :start_time => formatted_time_for(Time.now), - :host => host, - :force_check => true, - :cmd_typ => 96, - :cmd_mod => 2 - }) - response.code == 200 && response.body =~ /successful/ + request = { + :start_time => formatted_time_for(Time.now), + :host => host, + :force_check => true, + :cmd_typ => COMMANDS[:schedule_host_check], + } + + post_command(request) end def schedule_service_check(host, service) - response = post(cmd_url, :body => { - :start_time => formatted_time_for(Time.now), - :host => host, - :service => service, - :force_check => true, - :cmd_typ => 7, - :cmd_mod => 2 - }) - response.code == 200 && response.body =~ /successful/ + request = { + :start_time => formatted_time_for(Time.now), + :host => host, + :service => service, + :force_check => true, + :cmd_typ => COMMANDS[:schedule_service_check], + } + + post_command(request) end + + def host_status(host) + host_status_url = "#{status_url}?host=#{host}&embedded=1&noheader=1&limit=0" + response = get(host_status_url) + raise "wtf #{host_status_url}? #{response.code}" unless response.code == 200 + + services = {} + parse_status_html(response) do |status| + services[status[:service]] = status + end + + services + end + def service_status(options = {}) params = {} { :host_status_types => :notification_host, @@ -213,18 +239,19 @@ :sorttype, :sortoption, :hostprops, :serviceprops, ).each do |key| - params[key.to_s] = options[:val] if !options[:val].nil? && options[:val].match(/^\d*$/) + params[key.to_s] = options[:val] if options[:val] && options[:val].match(/^\d*$/) end if @version == 3 params['servicegroup'] = options[:group] || 'all' params['style'] = 'detail' params['embedded'] = '1' params['noheader'] = '1' + params['limit'] = 0 else if options[:group] params['servicegroup'] = options[:group] params['style'] = 'detail' else @@ -244,81 +271,129 @@ end statuses end - def host_status(host) - host_status_url = "#{status_url}?host=#{host}&embedded=1&noheader=1" - response = get(host_status_url) + def hostgroups_summary(hostgroup = "all") + hostgroups_summary_url = "#{status_url}?hostgroup=#{hostgroup}&style=summary" + response = get(hostgroups_summary_url) - raise "wtf #{host_status_url}? #{response.code}" unless response.code == 200 + raise "wtf #{hostgroups_summary_url}? #{response.code}" unless response.code == 200 - services = {} - parse_status_html(response) do |status| - services[status[:service]] = status + hostgroups = {} + parse_summary_html(response) do |status| + hostgroups[status[:group]] = status end - services + hostgroups end + def servicegroups_summary(servicegroup = "all") + servicegroups_summary_url = "#{status_url}?servicegroup=#{servicegroup}&style=summary" + response = get(servicegroups_summary_url) + + raise "wtf #{servicegroups_summary_url}? #{response.code}" unless response.code == 200 + + servicegroups = {} + parse_summary_html(response) do |status| + servicegroups[status[:group]] = status + end + + servicegroups + end + + def hostgroups_detail(hostgroup = "all") + hostgroups_detail_url = "#{status_url}?hostgroup=#{hostgroup}&style=hostdetail&embedded=1&noheader=1&limit=0" + response = get(hostgroups_detail_url) + + raise "wtf #{hostgroups_detail_url}? #{response.code}" unless response.code == 200 + + hosts = {} + parse_detail_html(response) do |status| + hosts[status[:host]] = status + end + + hosts + end + def disable_service_notifications(host, service, options = {}) request = { - :cmd_mod => 2, :host => host } if service - request[:cmd_typ] = 23 + request[:cmd_typ] = COMMANDS[:disable_service_notifications] request[:service] = service else - request[:cmd_typ] = 29 + request[:cmd_typ] = COMMANDS[:disable_host_service_checks] request[:ahas] = true end - response = post(cmd_url, :body => request) - if response.code == 200 && response.body =~ /successful/ - # TODO enable waiting. seems to hang intermittently - #if options[:wait] - # sleep(3) until service_notifications_disabled?(host, service) - #end - true - else - false - end + # TODO add option to block until the service shows as disabled + post_command(request) end def enable_service_notifications(host, service, options = {}) request = { - :cmd_mod => 2, :host => host } if service - request[:cmd_typ] = 22 + request[:cmd_typ] = COMMANDS[:enable_service_notifications] request[:service] = service else - request[:cmd_typ] = 28 + request[:cmd_typ] = COMMANDS[:enable_host_service_notifications] request[:ahas] = true end - response = post(cmd_url, :body => request) - if response.code == 200 && response.body =~ /successful/ - # TODO enable waiting. seems to hang intermittently - #if options[:wait] - # sleep(3) while service_notifications_disabled?(host, service) - #end - true - else - false - end + # TODO add option to block until the service shows as disabled + post_command(request) end def service_notifications_disabled?(host, service) self.host_status(host)[service].notifications_disabled end + + def alert_history(options = {}) + params = {} + + { + :state_type => :history_state, + :type => :history + }.each do |key, val| + if options[key] && (options[key].is_a?(Array) || options[key].is_a?(Symbol)) + params[key.to_s.gsub(/_/, '')] = Nagiosharder::Filters.value(val, *options[key]) + end + end + + # if any of the standard filter params are already integers, those win + %w( + :statetype, + :type + ).each do |key| + params[key.to_s] = options[:val] if !options[:val].nil? && options[:val].match(/^\d*$/) + end + + params['host'] = options[:host] || 'all' + params['archive'] = options[:archive] || '0' + + query = params.select {|k,v| v }.map {|k,v| "#{k}=#{v}" }.join('&') + + alert_history_url = "#{history_url}?#{query}" + puts alert_history_url + response = get(alert_history_url) + raise "wtf #{alert_history_url}? #{response.code}" unless response.code == 200 + alerts = [] + parse_history_html(response) do |alert| + alerts << alert + end + + alerts + end + def status_url "#{nagios_url}/status.cgi" end def cmd_url @@ -326,21 +401,127 @@ end def extinfo_url "#{nagios_url}/extinfo.cgi" end + + def history_url + "#{nagios_url}/history.cgi" + end private def formatted_time_for(time) time.strftime(nagios_time_format) end - def parse_status_html(response) + def parse_summary_html(response) doc = Nokogiri::HTML(response.to_s) rows = doc.css('table.status > tr') + rows.each do |row| + columns = Nokogiri::HTML(row.inner_html).css('body > td').to_a + if columns.any? + + # Group column + group = columns[0].inner_text.gsub(/\n/, '').match(/\((.*?)\)/)[1] + end + + if group + host_status_url, host_status_counts = parse_host_status_summary(columns[1]) if columns[1] + service_status_url, service_status_counts = parse_service_status_summary(columns[2]) if columns[2] + + status = Hashie::Mash.new :group => group, + :host_status_url => host_status_url, + :host_status_counts => host_status_counts, + :service_status_url => service_status_url, + :service_status_counts => service_status_counts + + yield status + end + end + end + + def parse_host_status_summary(column) + text = column.css('td a')[0] + link = text['href'] rescue nil + counts = {} + counts['up'] = column.inner_text.match(/(\d+)\s(UP)/)[1] rescue 0 + counts['down'] = column.inner_text.match(/(\d+)\s(DOWN)/)[1] rescue 0 + return link, counts + end + + def parse_service_status_summary(column) + text = column.css('td a')[0] + link = text['href'] rescue nil + counts = {} + counts['ok'] = column.inner_text.match(/(\d+)\s(OK)/)[1] rescue 0 + counts['warning'] = column.inner_text.match(/(\d+)\s(WARNING)/)[1] rescue 0 + counts['critical'] = column.inner_text.match(/(\d+)\s(CRITICAL)/)[1] rescue 0 + counts['unknown'] = column.inner_text.match(/(\d+)\s(UNKNOWN)/)[1] rescue 0 + return link, counts + end + + def parse_detail_html(response) + doc = Nokogiri::HTML(response.to_s) + rows = doc.css('table.status > tr') + + rows.each do |row| + columns = Nokogiri::HTML(row.inner_html).css('body > td').to_a + if columns.any? + + # Host column + host = columns[0].css('a').text.strip + + # Status + status = columns[1].inner_html if columns[1] + + # Last Check + last_check = if columns[2] && columns[2].inner_html != 'N/A' + last_check_str = columns[2].inner_html + debug "Need to parse #{columns[2].inner_html} in #{nagios_time_format}" + DateTime.strptime(columns[2].inner_html, nagios_time_format).to_s + end + debug 'parsed last check column' + + # Duration + duration = columns[3].inner_html.squeeze(' ').gsub(/^ /, '') if columns[3] + started_at = if duration && match_data = duration.match(/^\s*(\d+)d\s+(\d+)h\s+(\d+)m\s+(\d+)s\s*$/) + ( + match_data[1].to_i.days + + match_data[2].to_i.hours + + match_data[3].to_i.minutes + + match_data[4].to_i.seconds + ).ago + end + debug 'parsed duration column' + + # Status info + status_info = columns[4].inner_html.gsub('&nbsp;', '').gsub("\302\240", '').gsub("&#160;", '') if columns[4] + debug 'parsed status info column' + + if host && status && last_check && duration && started_at && status_info + host_extinfo_url = "#{extinfo_url}?type=1&host=#{host}" + + status = Hashie::Mash.new :host => host, + :status => status, + :last_check => last_check, + :duration => duration, + :started_at => started_at, + :extended_info => status_info, + :host_extinfo_url => host_extinfo_url + + yield status + end + end + end + end + + def parse_status_html(response) + doc = Nokogiri::HTML(response.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').to_s) + rows = doc.css('table.status > tr') + last_host = nil rows.each do |row| columns = Nokogiri::HTML(row.inner_html).css('body > td').to_a if columns.any? @@ -372,14 +553,19 @@ end acknowledged = other_links.any? do |link| link.css('img').attribute('src').to_s =~ /ack\.gif/ end + notifications_disabled = other_links.any? do |link| link.css('img').attribute('src').to_s =~ /ndisabled\.gif/ end + downtime = other_links.any? do |link| + link.css('img').attribute('src').to_s =~ /downtime\.gif/ + end + extra_service_notes_link = other_links.detect do |link| link.css('img').any? do |img| img.attribute('src').to_s =~ /notes\.gif/ end end @@ -439,17 +625,56 @@ :acknowledged => acknowledged, :service_extinfo_url => service_extinfo_url, :flapping => flapping, :comments_url => comments_url, :extra_service_notes_url => extra_service_notes_url, - :notifications_disabled => notifications_disabled + :notifications_disabled => notifications_disabled, + :downtime => downtime yield status end end end nil + end + + def parse_history_html(response) + doc = Nokogiri::HTML(response.to_s) + alerts = doc.css('div.logEntries img') + + if alerts.any? + alerts.each do |row| + text = row.next.text.gsub(' ',';').split(/;|: /) unless row.next.text.nil? + # differentiate host vs service alert output in nagios.log + case + when text.length >= 8 # service alert + last_check, alert_type, host, service, status, state, attempt, *extended_info = row.next.text.gsub(' ',';').split(/;|: /) + when text.length == 7 # host alert + last_check, alert_type, host, status, state, attempt, *extended_info = row.next.text.gsub(' ',';').split(/;|: /) + when text.length == 6 # service flapping alert + last_check, alert_type, host, service, status, state, *extended_info = row.next.text.gsub(' ',';').split(/;|: /) + when text.length == 5 # scheduled host downtime + last_check, alert_type, host, status, *extended_info = row.next.text.gsub(' ',';').split(/;|: /) + end + + service_extinfo_url = service ? "#{extinfo_url}?type=2&host=#{host}&service=#{CGI.escape(service)}" : nil + host_extinfo_url = "#{extinfo_url}?type=1&host=#{host}" + + alert = Hashie::Mash.new :last_check => last_check.gsub('[','').gsub(']',''), + :alert_type => alert_type, + :host => host, + :service => service, + :status => status, + :state => state, + :attempt => attempt, + :extended_info => extended_info.nil? ? extended_info : extended_info.join(': ').strip, + :host_extinfo_url => host_extinfo_url, + :service_extinfo_url => service_extinfo_url + + yield alert + end + end end def debug(*args) $stderr.puts *args if ENV['DEBUG'] end