systemd_mon_mod 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ require 'systemd_mon/error'
2
+ require 'systemd_mon/logger'
3
+
4
+ module SystemdMon
5
+ class NotificationCentre
6
+ include Enumerable
7
+
8
+ def initialize
9
+ self.notifiers = []
10
+ end
11
+
12
+ def classes
13
+ notifiers.map(&:class)
14
+ end
15
+
16
+ def each
17
+ notifiers.each do |notifier|
18
+ yield notifier
19
+ end
20
+ end
21
+
22
+ def add_notifier(notifier)
23
+ unless notifier.respond_to?(:notify!)
24
+ raise NotifierError, "Notifier #{notifier.class} must respond to 'notify!'"
25
+ end
26
+ self.notifiers << notifier
27
+ end
28
+
29
+ def notify_start!(hostname)
30
+ each_notifier do |notifier|
31
+ if notifier.respond_to?(:notify_start!)
32
+ Logger.puts "Notifying SystemdMon start via #{notifier.class}"
33
+ notifier.notify_start! hostname
34
+ else
35
+ Logger.debug { "#{notifier.class} doesn't respond to 'notify_start!', not sending notification" }
36
+ end
37
+ end
38
+ end
39
+
40
+ def notify_stop!(hostname)
41
+ each_notifier do |notifier|
42
+ if notifier.respond_to?(:notify_stop!)
43
+ Logger.puts "Notifying SystemdMon stop via #{notifier.class}"
44
+ notifier.notify_stop! hostname
45
+ else
46
+ Logger.debug { "#{notifier.class} doesn't respond to 'notify_start!', not sending notification" }
47
+ end
48
+ end
49
+ end
50
+
51
+ def notify!(notification)
52
+ each_notifier do |notifier|
53
+ Logger.puts "Notifying state change of #{notification.unit.name} via #{notifier.class}"
54
+ notifier.notify! notification
55
+ end
56
+ end
57
+
58
+ alias :<< :add_notifier
59
+
60
+ protected
61
+ attr_accessor :notifiers
62
+
63
+ def each_notifier
64
+ notifiers.map { |notifier|
65
+ Thread.new do
66
+ begin
67
+ yield notifier
68
+ rescue => e
69
+ Logger.error "Failed to send notification via #{notifier.class}:\n"
70
+ Logger.error " #{e.class}: #{e.message}\n"
71
+ Logger.debug_error { "\n\t#{e.backtrace.join('\n\t')}\n" }
72
+ end
73
+ end
74
+ }.each(&:join)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ module SystemdMon
2
+ class NotifierLoader
3
+ def get_class(name)
4
+ class_name = camel_case(name)
5
+ get_class_const(class_name)
6
+ rescue NameError
7
+ require "systemd_mon/notifiers/#{name}"
8
+ get_class_const(class_name)
9
+ end
10
+
11
+ protected
12
+ def camel_case(name)
13
+ return name if name !~ /_/ && name =~ /[A-Z]+.*/
14
+ name.split('_').map { |e| e.capitalize }.join
15
+ end
16
+
17
+ def get_class_const(name)
18
+ Notifiers.const_get(name)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require 'systemd_mon/logger'
2
+
3
+ module SystemdMon::Notifiers
4
+ class Base
5
+ def initialize(options)
6
+ self.options = options
7
+ self.me = self.class.name
8
+ end
9
+
10
+ # Subclasses must respond to a unit change
11
+ def notify!(notification)
12
+ raise "Notifier #{self.class} does not respond to notify!"
13
+ end
14
+
15
+ # Subclasses can choose to do something when SystemdMon starts
16
+ # E.g. with
17
+ #
18
+ # def notify_start!(hostname)
19
+ # end
20
+
21
+ # Subclasses can choose to do something when SystemdMon stops
22
+ # E.g. with
23
+ #
24
+ # def notify_stop!(hostname)
25
+ # end
26
+
27
+ def log(message)
28
+ SystemdMon::Logger.puts "#{me}: #{message}"
29
+ end
30
+
31
+ def debug(message = nil, &blk)
32
+ message = "#{me}: #{message}" if message
33
+ SystemdMon::Logger.debug message, &blk
34
+ end
35
+
36
+ protected
37
+ attr_accessor :options, :me
38
+ end
39
+ end
@@ -0,0 +1,65 @@
1
+ require 'systemd_mon/error'
2
+ require 'systemd_mon/notifiers/base'
3
+ require 'systemd_mon/formatters/state_table_formatter'
4
+
5
+ begin
6
+ require 'mail'
7
+ rescue LoadError
8
+ raise SystemdMon::NotifierDependencyError, "The 'mail' gem is required by the email notifier"
9
+ end
10
+
11
+ module SystemdMon::Notifiers
12
+ class Email < Base
13
+ def initialize(*)
14
+ super
15
+ if options['smtp']
16
+ opts = options
17
+ Mail.defaults do
18
+ delivery_method :smtp, Hash[opts['smtp'].map { |h, k| [h.to_sym, k] }]
19
+ end
20
+ end
21
+
22
+ validate_options!
23
+ end
24
+
25
+ def notify!(notification)
26
+ unit = notification.unit
27
+ subject = "#{notification.type_text}: #{unit.name} on #{notification.hostname}: #{unit.state_change.status_text}"
28
+ message = "Systemd unit #{unit.name} on #{notification.hostname} #{unit.state_change.status_text}: #{unit.state.active} (#{unit.state.sub})\n\n"
29
+ if unit.state_change.length > 1
30
+ message << SystemdMon::Formatters::StateTableFormatter.new(unit).as_text
31
+ end
32
+ message << "\nRegards, SystemdMon"
33
+
34
+ send_mail subject, message
35
+
36
+ log "sent email notification"
37
+ end
38
+
39
+ protected
40
+ attr_accessor :options
41
+
42
+ def validate_options!
43
+ unless options.has_key?("to")
44
+ raise NotifierError, "The 'to' address must be set to use the email notifier"
45
+ end
46
+ true
47
+ end
48
+
49
+ def send_mail(subject, message)
50
+ debug("Sending email to #{options['to']}:")
51
+ debug(%Q{ -> Subject: "#{subject}"})
52
+ debug(%Q{ -> Message: "#{message}"})
53
+
54
+ mail = Mail.new do
55
+ subject subject
56
+ body message
57
+ end
58
+ mail.to = options['to']
59
+ if options['from']
60
+ mail.from options['from']
61
+ end
62
+ mail.deliver!
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,62 @@
1
+ require 'systemd_mon/error'
2
+ require 'systemd_mon/notifiers/base'
3
+
4
+ begin
5
+ require 'hipchat'
6
+ rescue LoadError
7
+ raise SystemdMon::NotifierDependencyError, "The 'hipchat' gem is required by the hipchat notifier"
8
+ end
9
+
10
+ module SystemdMon::Notifiers
11
+ class Hipchat < Base
12
+ def initialize(*)
13
+ super
14
+ self.client = ::HipChat::Client.new(
15
+ options['token'],
16
+ :api_version => 'v2')
17
+ end
18
+
19
+ def notify_start!(hostname)
20
+ chat "SystemdMon is starting on #{hostname}",
21
+ 'green'
22
+ end
23
+
24
+ def notify_stop!(hostname)
25
+ chat "SystemdMon is stopping on #{hostname}",
26
+ 'yellow'
27
+ end
28
+
29
+ def notify!(notification)
30
+ unit = notification.unit
31
+ message = "#{notification.type_text}: systemd unit #{unit.name} on #{notification.hostname} #{unit.state_change.status_text}: #{unit.state.active} (#{unit.state.sub})"
32
+
33
+ chat message,
34
+ color(notification.type)
35
+
36
+ log "sent hipchat notification"
37
+ end
38
+
39
+ protected
40
+ attr_accessor :client, :options
41
+
42
+ def chat(message, shade)
43
+ client[options['room']].send(
44
+ options['username'],
45
+ message,
46
+ :color => shade)
47
+ end
48
+
49
+ def color(type)
50
+ case type
51
+ when :alert
52
+ 'red'
53
+ when :warning
54
+ 'yellow'
55
+ when :info
56
+ 'purple'
57
+ else
58
+ 'green'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,99 @@
1
+ require 'systemd_mon/error'
2
+ require 'systemd_mon/notifiers/base'
3
+
4
+ begin
5
+ require 'slack-notifier'
6
+ rescue LoadError
7
+ raise SystemdMon::NotifierDependencyError, "The 'slack-notifier' gem (> 1.0) is required by the slack notifier"
8
+ end
9
+
10
+ module SystemdMon::Notifiers
11
+ class Slack < Base
12
+ def initialize(*)
13
+ super
14
+ self.notifier = ::Slack::Notifier.new(
15
+ options.fetch('webhook_url'),
16
+ channel: options['channel'],
17
+ username: options['username'],
18
+ icon_emoji: options['icon_emoji'],
19
+ icon_url: options['icon_url'])
20
+ end
21
+
22
+ def notify_start!(hostname)
23
+ message = "@channel SystemdMon is starting on #{hostname}"
24
+
25
+ attach = {
26
+ fallback: message,
27
+ text: message,
28
+ color: "good"
29
+ }
30
+
31
+ notifier.ping "", attachments: [attach]
32
+ end
33
+
34
+ def notify_stop!(hostname)
35
+ message = "@channel SystemdMon is stopping on #{hostname}"
36
+
37
+ attach = {
38
+ fallback: message,
39
+ text: message,
40
+ color: "danger"
41
+ }
42
+
43
+ notifier.ping "", attachments: [attach]
44
+ end
45
+
46
+ def notify!(notification)
47
+ unit = notification.unit
48
+ message = "@channel #{notification.type_text}: systemd unit #{unit.name} on #{notification.hostname} #{unit.state_change.status_text}"
49
+
50
+ attach = {
51
+ fallback: "@channel #{message}: #{unit.state.active} (#{unit.state.sub})",
52
+ color: color(notification.type),
53
+ fields: fields(notification)
54
+ }
55
+
56
+ debug("sending slack message with attachment: ")
57
+ debug(attach.inspect)
58
+
59
+ notifier.ping message, attachments: [attach]
60
+ log "sent slack notification"
61
+ end
62
+
63
+ protected
64
+ attr_accessor :notifier
65
+
66
+ def fields(notification)
67
+ f = [
68
+ {
69
+ title: "Hostname",
70
+ value: notification.hostname,
71
+ short: true
72
+ },
73
+ {
74
+ title: "Unit",
75
+ value: notification.unit.name,
76
+ short: true
77
+ }
78
+ ]
79
+
80
+ changes = notification.unit.state_change.diff.map(&:last)
81
+ f.concat(changes.map { |v|
82
+ { title: v.display_name, value: v.value, short: true }
83
+ })
84
+ end
85
+
86
+ def color(type)
87
+ case type
88
+ when :alert
89
+ 'danger'
90
+ when :warning
91
+ '#FF9900'
92
+ when :info
93
+ '#0099CC'
94
+ else
95
+ 'good'
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,59 @@
1
+ require 'systemd_mon/state_value'
2
+
3
+ module SystemdMon
4
+ class State
5
+ include Enumerable
6
+
7
+ attr_reader :active, :sub, :loaded, :unit_file, :all_states
8
+
9
+ def initialize(active, sub, loaded, unit_file, type=nil)
10
+ timestamp = Time.now
11
+ @active = StateValue.new("active", active, timestamp, *active_states(type))
12
+ @sub = StateValue.new("status", sub, timestamp)
13
+ @loaded = StateValue.new("loaded", loaded, timestamp, %w(loaded))
14
+ @unit_file = StateValue.new("file", unit_file, timestamp, *file_states(type))
15
+ @all_states = [@active, @sub, @loaded, @unit_file]
16
+ end
17
+
18
+ def active_states(type)
19
+ case type
20
+ when 'oneshot'
21
+ [%w(inactive), %w(failed)]
22
+ else
23
+ [%w(active), %w(inactive failed)]
24
+ end
25
+ end
26
+
27
+ def file_states(type)
28
+ case type
29
+ when 'oneshot'
30
+ [[], []]
31
+ else
32
+ [%w(enabled linked-runtime static), %w(disabled)]
33
+ end
34
+ end
35
+
36
+ def each
37
+ @all_states.each do |state|
38
+ yield state
39
+ end
40
+ end
41
+
42
+ def ok?
43
+ all?(&:ok?)
44
+ end
45
+
46
+ def fail?
47
+ any?(&:fail?)
48
+ end
49
+
50
+ def to_s
51
+ @all_states.join(', ')
52
+ end
53
+
54
+ def ==(other)
55
+ @all_states == other.all_states
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,119 @@
1
+ module SystemdMon
2
+ class StateChange
3
+ include Enumerable
4
+
5
+ attr_reader :states
6
+
7
+ def initialize(original_state = nil)
8
+ self.states = []
9
+ states << original_state if original_state
10
+ end
11
+
12
+ def last
13
+ states.last
14
+ end
15
+
16
+ def length
17
+ states.length
18
+ end
19
+
20
+ def <<(state)
21
+ self.states << state
22
+ @diff = nil
23
+ end
24
+
25
+ def each
26
+ states.each do |state|
27
+ yield state
28
+ end
29
+ end
30
+
31
+ def changes
32
+ states[1..-1]
33
+ end
34
+
35
+ def recovery?
36
+ first.fail? && last.ok?
37
+ end
38
+
39
+ def ok?
40
+ last.ok?
41
+ end
42
+
43
+ def fail?
44
+ last.fail?
45
+ end
46
+
47
+ def restart?
48
+ first.ok? && last.ok? && changes.any? { |s| s.active == "deactivating" }
49
+ end
50
+
51
+ def auto_restart?
52
+ first.ok? && last.ok? && changes.any? { |s| s.sub == "auto-restart" }
53
+ end
54
+
55
+ def reload?
56
+ first.ok? && last.ok? && changes.any? { |s| s.active == "reloading" }
57
+ end
58
+
59
+ def still_fail?
60
+ length > 1 && first.fail? && last.fail?
61
+ end
62
+
63
+ def status_text
64
+ if recovery?
65
+ "recovered"
66
+ elsif auto_restart?
67
+ "automatically restarted"
68
+ elsif restart?
69
+ "restarted"
70
+ elsif reload?
71
+ "reloaded"
72
+ elsif still_fail?
73
+ "still failed"
74
+ elsif fail?
75
+ "failed"
76
+ else
77
+ "started"
78
+ end
79
+ end
80
+
81
+ def important?
82
+ if length == 1
83
+ first.fail?
84
+ else
85
+ diff.map(&:last).any?(&:important?)
86
+ end
87
+ end
88
+
89
+ def diff
90
+ @diff ||= zipped.reject { |states|
91
+ match = states.first.value
92
+ states.all? { |s| s.value == match }
93
+ }
94
+ end
95
+
96
+ def zipped
97
+ if length == 1
98
+ first.all_states
99
+ else
100
+ first.all_states.zip(*changes.map(&:all_states))
101
+ end
102
+ end
103
+
104
+ def to_s
105
+ diff.inject("") { |s, (*states)|
106
+ first = states.shift
107
+ s << "#{first.name} state changed from #{first.value} to "
108
+ s << states.map(&:value).join(" then ")
109
+ s << "\n"
110
+ s
111
+ }
112
+ end
113
+
114
+ protected
115
+ attr_accessor :original, :changed
116
+ attr_writer :states
117
+ end
118
+
119
+ end
@@ -0,0 +1,48 @@
1
+ module SystemdMon
2
+ class StateValue
3
+ attr_reader :name, :value, :ok_states, :failure_states, :timestamp
4
+
5
+ def initialize(name, value, timestamp, ok_states = [], failure_states = [])
6
+ self.name = name
7
+ self.value = value
8
+ self.ok_states = ok_states
9
+ self.failure_states = failure_states
10
+ self.timestamp = timestamp
11
+ end
12
+
13
+ def display_name
14
+ name.capitalize
15
+ end
16
+
17
+ def important?
18
+ ok_states.include?(value) || failure_states.include?(value)
19
+ end
20
+
21
+ def ok?
22
+ if ok_states.any?
23
+ ok_states.include?(value)
24
+ else
25
+ true
26
+ end
27
+ end
28
+
29
+ def fail?
30
+ if failure_states.any?
31
+ failure_states.include?(value)
32
+ else
33
+ false
34
+ end
35
+ end
36
+
37
+ def to_s
38
+ value
39
+ end
40
+
41
+ def ==(other)
42
+ other.is_a?(SystemdMon::StateValue) && value == other.value
43
+ end
44
+
45
+ protected
46
+ attr_writer :name, :value, :ok_states, :failure_states, :timestamp
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ require 'systemd_mon/state_change'
2
+
3
+ module SystemdMon
4
+ class UnitWithState
5
+ attr_reader :unit, :state_change
6
+
7
+ def initialize(unit)
8
+ self.unit = unit
9
+ self.state_change = StateChange.new
10
+ end
11
+
12
+ def name
13
+ unit.name
14
+ end
15
+
16
+ def <<(state)
17
+ self.state_change << state
18
+ end
19
+
20
+ def current_state
21
+ state_change.last
22
+ end
23
+
24
+ def reset!
25
+ self.state_change = StateChange.new(current_state)
26
+ end
27
+
28
+ alias :state :current_state
29
+
30
+ protected
31
+ attr_writer :state_change, :unit
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module SystemdMon
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "systemd_mon/version"
2
+ require "systemd_mon/logger"
3
+ require "systemd_mon/notifier_loader"
4
+
5
+ module SystemdMon; end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'systemd_mon/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "systemd_mon_mod"
8
+ spec.version = SystemdMon::VERSION
9
+ spec.authors = ["Jon Cairns", "Dmitriy Maksakov"]
10
+ spec.email = ["jon@joncairns.com", "mksvdmtr@yandex.ru"]
11
+ spec.summary = %q{Monitor systemd units and trigger alerts for failed states (Mod for mattermost @channel mentioning)}
12
+ spec.description = %q{Monitor systemd units and trigger alerts for failed states (Mod for mattermost @channel mentioning)}
13
+ spec.homepage = "https://github.com/mksvdmtr/systemd_mon_mod"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "ruby-dbus", "~> 0.11.0"
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake"
24
+ end