From 5ac1bd108ac43beed86eb486e022e375f56bc490 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 12 Apr 2016 15:32:45 +0200 Subject: [PATCH] Added nagios integration. --- app/models/channel/filter/icinga.rb | 78 +------- app/models/channel/filter/monitoring_base.rb | 88 +++++++++ app/models/channel/filter/nagios.rb | 7 + .../20160412000003_add_nagios_integration.rb | 98 ++++++++++ db/seeds.rb | 94 +++++++++ test/unit/integration_icinga_test.rb | 15 +- test/unit/integration_nagios_test.rb | 184 ++++++++++++++++++ 7 files changed, 482 insertions(+), 82 deletions(-) create mode 100644 app/models/channel/filter/monitoring_base.rb create mode 100644 app/models/channel/filter/nagios.rb create mode 100644 db/migrate/20160412000003_add_nagios_integration.rb create mode 100644 test/unit/integration_nagios_test.rb diff --git a/app/models/channel/filter/icinga.rb b/app/models/channel/filter/icinga.rb index 71567212a..3a830f3e0 100644 --- a/app/models/channel/filter/icinga.rb +++ b/app/models/channel/filter/icinga.rb @@ -1,83 +1,7 @@ # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ -module Channel::Filter::Icinga +class Channel::Filter::Icinga < Channel::Filter::MonitoringBase # rubocop:disable Style/ClassVars @@integration = 'icinga' # rubocop:enable Style/ClassVars - - # according - # https://github.com/Icinga/icinga2/blob/master/etc/icinga2/scripts/mail-service-notification.sh - # http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/monitoring-basics#host-states - # http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/monitoring-basics#service-states - - def self.run(_channel, mail) - - return if !Setting.get("#{@@integration}_integration") - sender = Setting.get("#{@@integration}_sender") - auto_close = Setting.get("#{@@integration}_auto_close") - auto_close_state_id = Setting.get("#{@@integration}_auto_close_state_id") - state_recovery_match = 'OK' - - return if !mail[:from] - return if !mail[:body] - sender_user_id = mail[ 'x-zammad-customer-id'.to_sym ] - return if !sender_user_id - - # check if sender is monitoring - return if !mail[:from].match(/#{sender}/i) - - # get mail attibutes like host and state - result = {} - mail[:body].gsub(%r{(Service|Host|State|Address|Date/Time|Additional\sInfo):(.+?)\n}i) { |_match| - key = $1 - if key - key = key.downcase - end - value = $2 - if value - value.strip! - end - result[key] = value - } - - # check if ticket with host is open - customer = User.lookup(id: sender_user_id) - - # follow up detection by meta data - open_states = Ticket::State.by_category('open') - Ticket.where(state: open_states).each {|ticket| - next if !ticket.preferences - next if !ticket.preferences['integration'] - next if ticket.preferences['integration'] != @@integration - next if !ticket.preferences[@@integration] - next if !ticket.preferences[@@integration]['host'] - next if ticket.preferences[@@integration]['host'] != result['host'] - next if ticket.preferences[@@integration]['service'] != result['service'] - - # found open ticket for service+host - mail[ 'x-zammad-ticket-id'.to_sym ] = ticket.id - - # check if service is recovered - if auto_close && result['state'].match(/#{state_recovery_match}/i) - state = Ticket::State.lookup(id: auto_close_state_id) - if state - mail[ 'x-zammad-ticket-followup-state'.to_sym ] = state.name - end - end - return true - } - - # new ticket, set meta data - if !mail[ 'x-zammad-ticket-id'.to_sym ] - if !mail[ 'x-zammad-ticket-preferences'.to_sym ] - mail[ 'x-zammad-ticket-preferences'.to_sym ] = {} - end - preferences = {} - preferences['integration'] = @@integration - preferences[@@integration] = result - preferences.each {|key, value| - mail[ 'x-zammad-ticket-preferences'.to_sym ][key] = value - } - end - end end diff --git a/app/models/channel/filter/monitoring_base.rb b/app/models/channel/filter/monitoring_base.rb new file mode 100644 index 000000000..1ffc2a13d --- /dev/null +++ b/app/models/channel/filter/monitoring_base.rb @@ -0,0 +1,88 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class Channel::Filter::MonitoringBase + # rubocop:disable Style/ClassVars + @@integration = 'to_be_defined' + # rubocop:enable Style/ClassVars + + # according + + # Icinga + # https://github.com/Icinga/icinga2/blob/master/etc/icinga2/scripts/mail-service-notification.sh + # http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/monitoring-basics#host-states + # http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/monitoring-basics#service-states + + # Nagios + # https://github.com/NagiosEnterprises/nagioscore/blob/754218e67653929a58938b99ef6b6039b6474fe4/sample-config/template-object/commands.cfg.in#L35 + + def self.run(_channel, mail) + + return if !Setting.get("#{@@integration}_integration") + sender = Setting.get("#{@@integration}_sender") + auto_close = Setting.get("#{@@integration}_auto_close") + auto_close_state_id = Setting.get("#{@@integration}_auto_close_state_id") + state_recovery_match = 'OK' + + return if !mail[:from] + return if !mail[:body] + sender_user_id = mail[ 'x-zammad-customer-id'.to_sym ] + return if !sender_user_id + + # check if sender is monitoring + return if !mail[:from].match(/#{sender}/i) + + # get mail attibutes like host and state + result = {} + mail[:body].gsub(%r{(Service|Host|State|Address|Date/Time|Additional\sInfo):(.+?)\n}i) { |_match| + key = $1 + if key + key = key.downcase + end + value = $2 + if value + value.strip! + end + result[key] = value + } + + # check if ticket with host is open + customer = User.lookup(id: sender_user_id) + + # follow up detection by meta data + open_states = Ticket::State.by_category('open') + Ticket.where(state: open_states).each {|ticket| + next if !ticket.preferences + next if !ticket.preferences['integration'] + next if ticket.preferences['integration'] != @@integration + next if !ticket.preferences[@@integration] + next if !ticket.preferences[@@integration]['host'] + next if ticket.preferences[@@integration]['host'] != result['host'] + next if ticket.preferences[@@integration]['service'] != result['service'] + + # found open ticket for service+host + mail[ 'x-zammad-ticket-id'.to_sym ] = ticket.id + + # check if service is recovered + if auto_close && result['state'].match(/#{state_recovery_match}/i) + state = Ticket::State.lookup(id: auto_close_state_id) + if state + mail[ 'x-zammad-ticket-followup-state'.to_sym ] = state.name + end + end + return true + } + + # new ticket, set meta data + if !mail[ 'x-zammad-ticket-id'.to_sym ] + if !mail[ 'x-zammad-ticket-preferences'.to_sym ] + mail[ 'x-zammad-ticket-preferences'.to_sym ] = {} + end + preferences = {} + preferences['integration'] = @@integration + preferences[@@integration] = result + preferences.each {|key, value| + mail[ 'x-zammad-ticket-preferences'.to_sym ][key] = value + } + end + end +end diff --git a/app/models/channel/filter/nagios.rb b/app/models/channel/filter/nagios.rb new file mode 100644 index 000000000..6dc01d41a --- /dev/null +++ b/app/models/channel/filter/nagios.rb @@ -0,0 +1,7 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class Channel::Filter::Nagios < Channel::Filter::MonitoringBase + # rubocop:disable Style/ClassVars + @@integration = 'nagios' + # rubocop:enable Style/ClassVars +end diff --git a/db/migrate/20160412000003_add_nagios_integration.rb b/db/migrate/20160412000003_add_nagios_integration.rb new file mode 100644 index 000000000..1212a79a8 --- /dev/null +++ b/db/migrate/20160412000003_add_nagios_integration.rb @@ -0,0 +1,98 @@ +class AddNagiosIntegration < ActiveRecord::Migration + def up + Setting.create_if_not_exists( + title: 'Define postmaster filter.', + name: '5100_postmaster_filter_nagios', + area: 'Postmaster::PreFilter', + description: 'Define postmaster filter for manage Nagios (http://www.nagios.org) emails.', + options: {}, + state: 'Channel::Filter::Nagios', + frontend: false + ) + Setting.create_if_not_exists( + title: 'Nagios integration', + name: 'nagios_integration', + area: 'Integration::Nagios', + description: 'Define if Nagios (http://www.nagios.org) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'nagios_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { prio: 1 }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'Sender', + name: 'nagios_sender', + area: 'Integration::Nagios', + description: 'Define the sender email address of Nagios emails.', + options: { + form: [ + { + display: '', + null: false, + name: 'nagios_sender', + tag: 'input', + }, + ], + }, + state: 'nagios@monitoring.example.com', + frontend: false, + preferences: { prio: 2 }, + ) + Setting.create_if_not_exists( + title: 'Auto close', + name: 'nagios_auto_close', + area: 'Integration::Nagios', + description: 'Define if tickets should be closed if service is recovered.', + options: { + form: [ + { + display: '', + null: true, + name: 'nagios_auto_close', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { prio: 3 }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'Auto close state', + name: 'nagios_auto_close_state_id', + area: 'Integration::Nagios', + description: 'Define the ticket state of auto closed tickets.', + options: { + form: [ + { + display: '', + null: false, + name: 'nagios_auto_close_state_id', + tag: 'select', + relation: 'TicketState', + }, + ], + }, + state: 4, + preferences: { prio: 4 }, + frontend: false + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 2f6fefa61..1d87da6a3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1588,6 +1588,15 @@ Setting.create_if_not_exists( state: 'Channel::Filter::Icinga', frontend: false ) +Setting.create_if_not_exists( + title: 'Define postmaster filter.', + name: '5100_postmaster_filter_nagios', + area: 'Postmaster::PreFilter', + description: 'Define postmaster filter for manage Nagios (http://www.nagios.org) emails.', + options: {}, + state: 'Channel::Filter::Nagios', + frontend: false +) Setting.create_if_not_exists( title: 'Icinga integration', name: 'icinga_integration', @@ -1673,6 +1682,91 @@ Setting.create_if_not_exists( preferences: { prio: 4 }, frontend: false ) +Setting.create_if_not_exists( + title: 'Nagios integration', + name: 'nagios_integration', + area: 'Integration::Nagios', + description: 'Define if Nagios (http://www.nagios.org) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'nagios_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { prio: 1 }, + frontend: false +) +Setting.create_if_not_exists( + title: 'Sender', + name: 'nagios_sender', + area: 'Integration::Nagios', + description: 'Define the sender email address of Nagios emails.', + options: { + form: [ + { + display: '', + null: false, + name: 'nagios_sender', + tag: 'input', + }, + ], + }, + state: 'nagios@monitoring.example.com', + frontend: false, + preferences: { prio: 2 }, +) +Setting.create_if_not_exists( + title: 'Auto close', + name: 'nagios_auto_close', + area: 'Integration::Nagios', + description: 'Define if tickets should be closed if service is recovered.', + options: { + form: [ + { + display: '', + null: true, + name: 'nagios_auto_close', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { prio: 3 }, + frontend: false +) +Setting.create_if_not_exists( + title: 'Auto close state', + name: 'nagios_auto_close_state_id', + area: 'Integration::Nagios', + description: 'Define the ticket state of auto closed tickets.', + options: { + form: [ + { + display: '', + null: false, + name: 'nagios_auto_close_state_id', + tag: 'select', + relation: 'TicketState', + }, + ], + }, + state: 4, + preferences: { prio: 4 }, + frontend: false +) signature = Signature.create_if_not_exists( id: 1, diff --git a/test/unit/integration_icinga_test.rb b/test/unit/integration_icinga_test.rb index 1c6e60637..b849a7166 100644 --- a/test/unit/integration_icinga_test.rb +++ b/test/unit/integration_icinga_test.rb @@ -3,6 +3,11 @@ require 'test_helper' class IntegrationIcingaTest < ActiveSupport::TestCase + # according + # https://github.com/Icinga/icinga2/blob/master/etc/icinga2/scripts/mail-service-notification.sh + # http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/monitoring-basics#host-states + # http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/monitoring-basics#service-states + test 'base tests' do Setting.set('icinga_integration', true) @@ -14,7 +19,7 @@ User-Agent: Heirloom mailx 12.5 7/5/10 MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable -Message-Id: <20160131094621.29ECD400F29C-1@monitoring.znuny.com> +Message-Id: <20160131094621.29ECD400F29C-icinga-1@monitoring.znuny.com> From: icinga_not_matching@monitoring.example.com (icinga) ***** Icinga ***** @@ -46,7 +51,7 @@ User-Agent: Heirloom mailx 12.5 7/5/10 MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable -Message-Id: <20160131094621.29ECD400F29C-2@monitoring.znuny.com> +Message-Id: <20160131094621.29ECD400F29C-icinga-2@monitoring.znuny.com> From: icinga@monitoring.example.com (icinga) ***** Icinga ***** @@ -82,7 +87,7 @@ User-Agent: Heirloom mailx 12.5 7/5/10 MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable -Message-Id: <20160131094621.29ECD400F29C-3@monitoring.znuny.com> +Message-Id: <20160131094621.29ECD400F29C-icinga-3@monitoring.znuny.com> From: icinga@monitoring.example.com (icinga) ***** Icinga ***** @@ -119,7 +124,7 @@ User-Agent: Heirloom mailx 12.5 7/5/10 MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable -Message-Id: <20160131094621.29ECD400F29C-4@monitoring.znuny.com> +Message-Id: <20160131094621.29ECD400F29C-icinga-4@monitoring.znuny.com> From: icinga@monitoring.example.com (icinga) ***** Icinga ***** @@ -156,7 +161,7 @@ User-Agent: Heirloom mailx 12.5 7/5/10 MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable -Message-Id: <20160131094621.29ECD400F29C-5@monitoring.znuny.com> +Message-Id: <20160131094621.29ECD400F29C-icinga-5@monitoring.znuny.com> From: icinga@monitoring.example.com (icinga) ***** Icinga ***** diff --git a/test/unit/integration_nagios_test.rb b/test/unit/integration_nagios_test.rb new file mode 100644 index 000000000..186e636c8 --- /dev/null +++ b/test/unit/integration_nagios_test.rb @@ -0,0 +1,184 @@ +# encoding: utf-8 +require 'test_helper' + +class IntegrationNagiosTest < ActiveSupport::TestCase + + # according + # https://github.com/NagiosEnterprises/nagioscore/blob/754218e67653929a58938b99ef6b6039b6474fe4/sample-config/template-object/commands.cfg.in#L35 + + test 'base tests' do + + Setting.set('nagios_integration', true) + + # not matching sender + email_raw_string = "To: support@example.com +Subject: ** PROBLEM Service Alert: host.internal.loc/CPU Load is WARNING ** +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: quoted-printable +Message-Id: <20160131094621.29ECD400F29C-nagios-1@monitoring.znuny.com> +From: nagios_not_matching@monitoring.example.com (nagios) + +***** Nagios ***** + +Notification Type: PROBLEM + +Service: CPU Load +Host: host.internal.loc +Address: 1.1.1.1 +State: PROBLEM + +Date/Time: 2016-01-31 10:46:20 +0100 + +Additional Info: +WARNING - load average: 3.44, 0.99, 0.35 +" + + ticket_p, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('new', ticket_p.state.name) + assert(ticket_p.preferences) + assert_not(ticket_p.preferences['integration']) + assert_not(ticket_p.preferences['nagios']) + + # matching sender - CPU Load/host.internal.loc + email_raw_string = "To: support@example.com +Subject: ** PROBLEM Service Alert: host.internal.loc/CPU Load is WARNING ** +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: quoted-printable +Message-Id: <20160131094621.29ECD400F29C-nagios-2@monitoring.znuny.com> +From: nagios@monitoring.example.com (nagios) + +***** Nagios ***** + +Notification Type: PROBLEM + +Service: CPU Load +Host: host.internal.loc +Address: 1.1.1.1 +State: WARNING + +Date/Time: 2016-01-31 10:46:20 +0100 + +Additional Info: +WARNING - load average: 3.44, 0.99, 0.35 +" + + ticket_1, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('new', ticket_1.state.name) + assert(ticket_1.preferences) + assert(ticket_1.preferences['integration']) + assert_equal('nagios', ticket_1.preferences['integration']) + assert(ticket_1.preferences['nagios']) + assert_equal('host.internal.loc', ticket_1.preferences['nagios']['host']) + assert_equal('CPU Load', ticket_1.preferences['nagios']['service']) + assert_equal('WARNING', ticket_1.preferences['nagios']['state']) + + # matching sender - Disk Usage 123/host.internal.loc + email_raw_string = "To: support@example.com +Subject: ** PROBLEM Service Alert: host.internal.loc/Disk Usage 123 is WARNING ** +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: quoted-printable +Message-Id: <20160131094621.29ECD400F29C-nagios-3@monitoring.znuny.com> +From: nagios@monitoring.example.com (nagios) + +***** Nagios ***** + +Notification Type: PROBLEM + +Service: Disk Usage 123 +Host: host.internal.loc +Address: 1.1.1.1 +State: WARNING + +Date/Time: 2016-01-31 10:46:20 +0100 + +Additional Info: +WARNING - load average: 3.44, 0.99, 0.35 +" + + ticket_2, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('new', ticket_2.state.name) + assert(ticket_2.preferences) + assert(ticket_2.preferences['integration']) + assert_equal('nagios', ticket_2.preferences['integration']) + assert(ticket_2.preferences['nagios']) + assert_equal('host.internal.loc', ticket_2.preferences['nagios']['host']) + assert_equal('Disk Usage 123', ticket_2.preferences['nagios']['service']) + assert_equal('WARNING', ticket_2.preferences['nagios']['state']) + assert_not_equal(ticket_2.id, ticket_1.id) + + # matching sender - follow up - CPU Load/host.internal.loc + email_raw_string = "To: support@example.com +Subject: ** PROBLEM Service Alert: host.internal.loc/CPU Load is WARNING ** +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: quoted-printable +Message-Id: <20160131094621.29ECD400F29C-nagios-4@monitoring.znuny.com> +From: nagios@monitoring.example.com (nagios) + +***** Nagios ***** + +Notification Type: PROBLEM + +Service: CPU Load +Host: host.internal.loc +Address: 1.1.1.1 +State: WARNING + +Date/Time: 2016-01-31 10:46:20 +0100 + +Additional Info: +WARNING - load average: 3.44, 0.99, 0.35 +" + + ticket_1_1, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('new', ticket_1_1.state.name) + assert(ticket_1_1.preferences) + assert(ticket_1_1.preferences['integration']) + assert_equal('nagios', ticket_1_1.preferences['integration']) + assert(ticket_1_1.preferences['nagios']) + assert_equal('host.internal.loc', ticket_1_1.preferences['nagios']['host']) + assert_equal('CPU Load', ticket_1_1.preferences['nagios']['service']) + assert_equal('WARNING', ticket_1_1.preferences['nagios']['state']) + assert_equal(ticket_1.id, ticket_1_1.id) + + # matching sender - follow up - recovery - CPU Load/host.internal.loc + email_raw_string = "To: support@example.com +Subject: ** PROBLEM Service Alert: host.internal.loc/CPU Load is WARNING ** +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: quoted-printable +Message-Id: <20160131094621.29ECD400F29C-nagios-5@monitoring.znuny.com> +From: nagios@monitoring.example.com (nagios) + +***** Nagios ***** + +Notification Type: PROBLEM + +Service: CPU Load +Host: host.internal.loc +Address: 1.1.1.1 +State: OK + +Date/Time: 2016-01-31 10:48:02 +0100 + +Additional Info: +" + ticket_1_2, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal(ticket_1.id, ticket_1_2.id) + assert_equal('closed', ticket_1_2.state.name) + assert(ticket_1_2.preferences) + assert(ticket_1_2.preferences['integration']) + assert_equal('nagios', ticket_1_2.preferences['integration']) + assert(ticket_1_2.preferences['nagios']) + assert_equal('host.internal.loc', ticket_1_2.preferences['nagios']['host']) + assert_equal('CPU Load', ticket_1_2.preferences['nagios']['service']) + assert_equal('WARNING', ticket_1_2.preferences['nagios']['state']) + + #Setting.set('nagios_integration', false) + + end + +end