diff --git a/app/assets/javascripts/app/controllers/_integration/monit.coffee b/app/assets/javascripts/app/controllers/_integration/monit.coffee new file mode 100644 index 000000000..208c6d507 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/monit.coffee @@ -0,0 +1,32 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'monit_integration' + featureName: 'Monit' + featureConfig: 'monit_config' + description: [ + ['This service receives emails from %s and creates tickets with host and service.', 'Monit'] + ['If the host and service is recovered again, the ticket will be closed automatically.'] + ] + + render: => + super + new App.SettingsForm( + area: 'Integration::Monit' + el: @$('.js-form') + ) + +class State + @current: -> + App.Setting.get('monit_integration') + +App.Config.set( + 'IntegrationMonit' + { + name: 'Monit' + target: '#system/integration/monit' + description: 'An open source monitoring tool.' + controller: Index + state: State + permission: ['admin.integration.monit'] + } + 'NavBarIntegrations' +) diff --git a/app/models/channel/filter/monit.rb b/app/models/channel/filter/monit.rb new file mode 100644 index 000000000..12a37b30e --- /dev/null +++ b/app/models/channel/filter/monit.rb @@ -0,0 +1,7 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class Channel::Filter::Monit < Channel::Filter::MonitoringBase + def self.integration_name + 'monit' + end +end diff --git a/app/models/channel/filter/monitoring_base.rb b/app/models/channel/filter/monitoring_base.rb index 97e7651a1..9ab171bc1 100644 --- a/app/models/channel/filter/monitoring_base.rb +++ b/app/models/channel/filter/monitoring_base.rb @@ -30,7 +30,7 @@ class Channel::Filter::MonitoringBase # get mail attibutes like host and state result = {} - mail[:body].gsub(%r{(Service|Host|State|Address|Date/Time|Additional\sInfo|Info):(.+?)\n}i) do |_match| + mail[:body].gsub(%r{(Service|Host|State|Address|Date/Time|Additional\sInfo|Info|Action|Description):(.+?)\n}i) do |_match| key = $1 if key key = key.downcase @@ -45,14 +45,14 @@ class Channel::Filter::MonitoringBase # check min. params return if result['host'].blank? - # get state by body - ichinga new templates + # icinga - get state by body - new templates if result['state'].blank? if mail[:body] =~ /==>.*\sis\s(.+?)\!\s+?<==/ result['state'] = $1 end end - # get state by subject - ichinga new templates "state:" is not in body anymore + # icinga - get state by subject - new templates "state:" is not in body anymore # Subject: [PROBLEM] Ping IPv4 on host1234.dc.example.com is WARNING! # Subject: [PROBLEM] Host host1234.dc.example.com is DOWN! if result['state'].blank? @@ -61,6 +61,24 @@ class Channel::Filter::MonitoringBase end end + # monit - get missing attributes from body + if result['service'].blank? + if mail[:body] =~ /\sService\s(.+?)\s/ + result['service'] = $1 + end + end + + # possible event types https://mmonit.com/monit/documentation/#Setting-an-event-filter + if result['state'].blank? + result['state'] = if mail[:body] =~ /\s(done|recovery|succeeded|bytes\sok|packets\sok)\s/ + 'OK' + elsif mail[:body] =~ /(instance\schanged\snot|Link\sup|Exists|Saturation\sok|Speed\sok)/ + 'OK' + else + 'CRITICAL' + end + end + # check if ticket with host is open customer = User.lookup(id: session_user_id) diff --git a/db/migrate/20171024000001_monit_integration.rb b/db/migrate/20171024000001_monit_integration.rb new file mode 100644 index 000000000..0e9e1009e --- /dev/null +++ b/db/migrate/20171024000001_monit_integration.rb @@ -0,0 +1,118 @@ +class MonitIntegration < ActiveRecord::Migration[4.2] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Monit integration', + name: 'monit_integration', + area: 'Integration::Switch', + description: 'Defines if Monit (https://mmonit.com/monit/) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'monit_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 1, + permission: ['admin.integration'], + }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'Sender', + name: 'monit_sender', + area: 'Integration::Monit', + description: 'Defines the sender email address of the service emails.', + options: { + form: [ + { + display: '', + null: false, + name: 'monit_sender', + tag: 'input', + placeholder: 'monit@monitoring.example.com', + }, + ], + }, + state: 'monit@monitoring.example.com', + preferences: { + prio: 2, + permission: ['admin.integration'], + }, + frontend: false, + ) + Setting.create_if_not_exists( + title: 'Auto close', + name: 'monit_auto_close', + area: 'Integration::Monit', + description: 'Defines if tickets should be closed if service is recovered.', + options: { + form: [ + { + display: '', + null: true, + name: 'monit_auto_close', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + translate: true, + }, + ], + }, + state: true, + preferences: { + prio: 3, + permission: ['admin.integration'], + }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'Auto close state', + name: 'monit_auto_close_state_id', + area: 'Integration::Monit', + description: 'Defines the state of auto closed tickets.', + options: { + form: [ + { + display: '', + null: false, + name: 'monit_auto_close_state_id', + tag: 'select', + relation: 'TicketState', + translate: true, + }, + ], + }, + state: 4, + preferences: { + prio: 4, + permission: ['admin.integration'], + }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'Defines postmaster filter.', + name: '5300_postmaster_filter_monit', + area: 'Postmaster::PreFilter', + description: 'Defines postmaster filter to manage Monit (https://mmonit.com/monit/) emails.', + options: {}, + state: 'Channel::Filter::Monit', + frontend: false + ) + end + +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index fcd368959..12b351599 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -2708,6 +2708,15 @@ Setting.create_if_not_exists( state: 'Channel::Filter::Nagios', frontend: false ) +Setting.create_if_not_exists( + title: 'Defines postmaster filter.', + name: '5300_postmaster_filter_monit', + area: 'Postmaster::PreFilter', + description: 'Defines postmaster filter to manage Monit (https://mmonit.com/monit/) emails.', + options: {}, + state: 'Channel::Filter::Monit', + frontend: false +) Setting.create_if_not_exists( title: 'Icinga integration', name: 'icinga_integration', @@ -3014,6 +3023,106 @@ Setting.create_if_not_exists( }, frontend: false ) +Setting.create_if_not_exists( + title: 'Monit integration', + name: 'monit_integration', + area: 'Integration::Switch', + description: 'Defines if Monit (https://mmonit.com/monit/) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'monit_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 1, + permission: ['admin.integration'], + }, + frontend: false +) +Setting.create_if_not_exists( + title: 'Sender', + name: 'monit_sender', + area: 'Integration::Monit', + description: 'Defines the sender email address of the service emails.', + options: { + form: [ + { + display: '', + null: false, + name: 'monit_sender', + tag: 'input', + placeholder: 'monit@monitoring.example.com', + }, + ], + }, + state: 'monit@monitoring.example.com', + preferences: { + prio: 2, + permission: ['admin.integration'], + }, + frontend: false, +) +Setting.create_if_not_exists( + title: 'Auto close', + name: 'monit_auto_close', + area: 'Integration::Monit', + description: 'Defines if tickets should be closed if service is recovered.', + options: { + form: [ + { + display: '', + null: true, + name: 'monit_auto_close', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + translate: true, + }, + ], + }, + state: true, + preferences: { + prio: 3, + permission: ['admin.integration'], + }, + frontend: false +) +Setting.create_if_not_exists( + title: 'Auto close state', + name: 'monit_auto_close_state_id', + area: 'Integration::Monit', + description: 'Defines the state of auto closed tickets.', + options: { + form: [ + { + display: '', + null: false, + name: 'monit_auto_close_state_id', + tag: 'select', + relation: 'TicketState', + translate: true, + }, + ], + }, + state: 4, + preferences: { + prio: 4, + permission: ['admin.integration'], + }, + frontend: false +) Setting.create_if_not_exists( title: 'LDAP integration', diff --git a/test/unit/integration_monit_test.rb b/test/unit/integration_monit_test.rb new file mode 100644 index 000000000..60b184021 --- /dev/null +++ b/test/unit/integration_monit_test.rb @@ -0,0 +1,206 @@ +# encoding: utf-8 +require 'test_helper' + +class IntegrationMoniTest < ActiveSupport::TestCase + + # according + # https://mmonit.com/monit/documentation/#ALERT-MESSAGES + + setup do + Setting.set('monit_integration', true) + Setting.set('monit_sender', 'monit@monitoring.example.com') + end + + test 'base tests' do + + # Service + email_raw_string = "Message-Id: <20160131094621.29ECD400F29C-monit-1-1@monitoring.znuny.com> +From: monit@monitoring.example.com +To: admin@example +Subject: monit alert -- Timeout php-fpm +Date: Thu, 24 Aug 2017 08:30:42 GMT +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 8bit +X-Mailer: Monit 5.23.0 +MIME-Version: 1.0 + +Timeout Service php-fpm + + Date: Thu, 24 Aug 2017 10:30:42 + Action: unmonitor + Host: web1.example + Description: service restarted 6 times within 3 cycles(s) - unmonitor + +Your faithful employee, +Monit +" + + ticket_0, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('new', ticket_0.state.name) + assert(ticket_0.preferences) + assert(ticket_0.preferences['monit']) + assert_equal('unmonitor', ticket_0.preferences['monit']['action']) + assert_equal('web1.example', ticket_0.preferences['monit']['host']) + assert_equal('service restarted 6 times within 3 cycles(s) - unmonitor', ticket_0.preferences['monit']['description']) + assert_equal('php-fpm', ticket_0.preferences['monit']['service']) + assert_equal('CRITICAL', ticket_0.preferences['monit']['state']) + + email_raw_string = "Message-Id: <20160131094621.29ECD400F29C-monit-1-2@monitoring.znuny.com> +From: monit@monitoring.example.com +To: admin@example +Subject: monit alert -- Action done php-fpm +Date: Thu, 24 Aug 2017 08:30:42 GMT +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 8bit +X-Mailer: Monit 5.23.0 +MIME-Version: 1.0 + +Action done Service php-fpm + + Date: Thu, 24 Aug 2017 10:37:39 + Action: alert + Host: web1.example + Description: monitor action done + +Your faithful employee, +Monit" + + ticket_0_1, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('closed', ticket_0_1.state.name) + assert(ticket_0_1.preferences) + assert(ticket_0_1.preferences['monit']) + assert_equal('unmonitor', ticket_0.preferences['monit']['action']) + assert_equal('web1.example', ticket_0_1.preferences['monit']['host']) + assert_equal('service restarted 6 times within 3 cycles(s) - unmonitor', ticket_0_1.preferences['monit']['description']) + assert_equal('php-fpm', ticket_0_1.preferences['monit']['service']) + assert_equal('CRITICAL', ticket_0_1.preferences['monit']['state']) + assert_equal(ticket_0_1.id, ticket_0.id) + + # Service + email_raw_string = "Message-Id: <20160131094621.29ECD400F29C-monit-2-1@monitoring.znuny.com> +From: monit@monitoring.example.com +To: admin@example +Subject: monit alert -- Connection failed host.example +Date: Thu, 24 Aug 2017 08:30:42 GMT +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +X-Mailer: Monit 5.23.0 +MIME-Version: 1.0 + +Connection failed Service host.example=20 + + Date: Fri, 25 Aug 2017 02:28:31 + Action: alert + Host: web5.host.example + Description: failed protocol test [HTTP] at [host.example]:80 [TCP/I= +P] -- HTTP: Error receiving data -- Resource temporarily unavailable + +Your faithful employee, +Monit" + + 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['monit']) + assert_equal('alert', ticket_1.preferences['monit']['action']) + assert_equal('web5.host.example', ticket_1.preferences['monit']['host']) + assert_equal('failed protocol test [HTTP] at [host.example]:80 [TCP/IP] -- HTTP: Error receiving data -- Resource temporarily unavailable', ticket_1.preferences['monit']['description']) + assert_equal('host.example', ticket_1.preferences['monit']['service']) + assert_equal('CRITICAL', ticket_1.preferences['monit']['state']) + + email_raw_string = "Message-Id: <20160131094621.29ECD400F29C-monit-2-2@monitoring.znuny.com> +From: monit@monitoring.example.com +To: admin@example +Subject: monit alert -- Connection succeeded host.example +Date: Thu, 24 Aug 2017 08:30:42 GMT +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +X-Mailer: Monit 5.23.0 +MIME-Version: 1.0 + +Connection succeeded Service host.example=20 + + Date: Fri, 25 Aug 2017 02:29:13 + Action: alert + Host: web5.host.example + Description: connection succeeded to [host.example]:80 [TCP/IP] + +Your faithful employee, +Monit" + + ticket_1_1, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('closed', ticket_1_1.state.name) + assert(ticket_1_1.preferences) + assert(ticket_1_1.preferences['monit']) + assert_equal('alert', ticket_1.preferences['monit']['action']) + assert_equal('web5.host.example', ticket_1_1.preferences['monit']['host']) + assert_equal('failed protocol test [HTTP] at [host.example]:80 [TCP/IP] -- HTTP: Error receiving data -- Resource temporarily unavailable', ticket_1_1.preferences['monit']['description']) + assert_equal('host.example', ticket_1_1.preferences['monit']['service']) + assert_equal('CRITICAL', ticket_1_1.preferences['monit']['state']) + assert_equal(ticket_1_1.id, ticket_1.id) + + # Resource Limit + email_raw_string = "Message-Id: <20160131094621.29ECD400F29C-monit-3-1@monitoring.znuny.com> +From: monit@monitoring.example.com +To: admin@example +Subject: monit alert -- Resource limit matched web5.example.net +Date: Thu, 24 Aug 2017 08:30:42 GMT +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +X-Mailer: Monit 5.23.0 +MIME-Version: 1.0 + +Resource limit matched Service web5.example.net=20 + + Date: Fri, 25 Aug 2017 02:02:08 + Action: exec + Host: web5.example.net + Description: loadavg(1min) of 10.7 matches resource limit [loadavg(1min) >= + 6.0] + +Your faithful employee, +Monit" + + 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['monit']) + assert_equal('exec', ticket_2.preferences['monit']['action']) + assert_equal('web5.example.net', ticket_2.preferences['monit']['host']) + assert_equal('loadavg(1min) of 10.7 matches resource limit [loadavg(1min) > 6.0]', ticket_2.preferences['monit']['description']) + assert_equal('web5.example.net', ticket_2.preferences['monit']['service']) + assert_equal('CRITICAL', ticket_2.preferences['monit']['state']) + + email_raw_string = "Message-Id: <20160131094621.29ECD400F29C-monit-3-2@monitoring.znuny.com> +From: monit@monitoring.example.com +To: admin@example +Subject: monit alert -- Resource limit succeeded web5.example.net +Date: Thu, 24 Aug 2017 08:30:42 GMT +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +X-Mailer: Monit 5.23.0 +MIME-Version: 1.0 + +Resource limit succeeded Service web5.example.net=20 + + Date: Fri, 25 Aug 2017 02:05:18 + Action: alert + Host: web5.example.net + Description: loadavg(1min) check succeeded [current loadavg(1min) =3D 4.8] + +Your faithful employee, +Monit" + + ticket_2_1, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal('closed', ticket_2_1.state.name) + assert(ticket_2_1.preferences) + assert(ticket_2_1.preferences['monit']) + assert_equal('exec', ticket_2.preferences['monit']['action']) + assert_equal('web5.example.net', ticket_2_1.preferences['monit']['host']) + assert_equal('loadavg(1min) of 10.7 matches resource limit [loadavg(1min) > 6.0]', ticket_2_1.preferences['monit']['description']) + assert_equal('web5.example.net', ticket_2_1.preferences['monit']['service']) + assert_equal('CRITICAL', ticket_2_1.preferences['monit']['state']) + assert_equal(ticket_2_1.id, ticket_2.id) + end + +end