zabbirc 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa6b2a62b76aecb1541a8608444b54aa0435258a
4
+ data.tar.gz: cd3e76f12c22caed15cf276cf4518dac82160946
5
+ SHA512:
6
+ metadata.gz: ec786a1141cff5e769b91cefe1ca45b9e2fa97b7f4a15ad70f5fa13c614acfb63c4674f606f365d473e2f92938589fc154703ea2cad767b661f8869196141be9
7
+ data.tar.gz: 3a0b23a5017ceb2f8aabdcc73af0a300bb2795595a2f813a77d1ff054a35d0ed469b7b44869c9a2dfd5071777eca7269b20af4d07651cb9bc907098fb5748a4a
data/bin/zabbirc ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ require 'zabbirc'
3
+ require 'pry'
4
+ this_file = __FILE__
5
+ config_path = File.expand_path(ARGV[0])
6
+
7
+ unless config_path
8
+ $stderr.puts "No config path specified"
9
+ exit false
10
+ end
11
+ unless File.exists?(config_path)
12
+ $stderr.puts "Could not find config file: #{config_path}"
13
+ exit false
14
+ end
15
+
16
+ require_relative config_path
17
+
18
+ Zabbirc.logger # initializes logger
19
+ exit false unless Zabbirc::Zabbix::Connection.test_connection
20
+
21
+ s = Zabbirc::Service.new
22
+
23
+ trap "SIGINT" do
24
+ s.stop
25
+ end
26
+
27
+ trap "SIGTERM" do
28
+ s.stop
29
+ end
30
+
31
+ s.start
32
+ s.join
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ case ARGV.size
3
+ when 0
4
+ install_path = Dir.pwd
5
+ when 1
6
+ install_path = ARGV[0]
7
+ when 2
8
+ puts "Too many arguments"
9
+ exit 1
10
+ end
11
+
12
+ puts "Installing config file into: #{install_path}"
13
+ templates_path = Pathname.new(File.expand_path(Pathname.new(File.dirname(__FILE__)).join("../templates")))
14
+
15
+ FileUtils.cp(templates_path.join("zabbirc_config.rb"), install_path)
16
+ puts "Installed"
data/config/config.rb ADDED
@@ -0,0 +1,5 @@
1
+ Zabbirc.configure do |config|
2
+ config.zabbix_api_url = ENV['ZABBIX_API_URL']
3
+ config.zabbix_login = ENV['ZABBIX_LOGIN']
4
+ config.zabbix_password = ENV['ZABBIX_PASSWORD']
5
+ end
@@ -0,0 +1,43 @@
1
+ require 'active_support/configurable'
2
+
3
+ module Zabbirc
4
+ def self.configure(&block)
5
+ block.call(@config ||= Zabbirc::Configuration.new)
6
+ end
7
+
8
+ def self.config
9
+ @config
10
+ end
11
+
12
+ class Configuration #:nodoc:
13
+ include ActiveSupport::Configurable
14
+
15
+ config_accessor :zabbix_api_url
16
+ config_accessor :zabbix_login
17
+ config_accessor :zabbix_password
18
+
19
+ config_accessor :irc_server
20
+ config_accessor :irc_channels
21
+
22
+ config_accessor :events_check_interval
23
+ config_accessor :notify_about_events_from_last
24
+
25
+ def param_name
26
+ config.param_name.respond_to?(:call) ? config.param_name.call : config.param_name
27
+ end
28
+
29
+ # define param_name writer (copied from AS::Configurable)
30
+ writer, line = 'def param_name=(value); config.param_name = value; end', __LINE__
31
+ singleton_class.class_eval writer, __FILE__, line
32
+ class_eval writer, __FILE__, line
33
+ end
34
+
35
+ # this is ugly. why can't we pass the default value to config_accessor...?
36
+ configure do |config|
37
+ config.events_check_interval = 10.seconds
38
+ config.notify_about_events_from_last = 5.minutes
39
+
40
+ config.irc_server = "irc.freenode.org"
41
+ config.irc_channels = ["#zabbirc-test", "#zabbirc-test-2"]
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "plugin_methods"
2
+
3
+ module Zabbirc
4
+ module Irc
5
+ class Plugin
6
+ include Cinch::Plugin
7
+ include PluginMethods
8
+
9
+ listen_to :join, method: :sync_ops
10
+ listen_to :leaving, method: :sync_ops
11
+
12
+ match "settings", method: :show_settings
13
+ match /settings set ([#_a-zA-Z0-9]+)( ([#\-_a-zA-Z0-9]+))?/, method: :set_setting
14
+ match "events", method: :list_events
15
+ match /status ([a-zA-Z0-9\-.]+)/, method: :host_status
16
+ match /latest ([a-zA-Z0-9\-.]+)( (\d+))?/, method: :host_latest
17
+ match /ack (\d+) (.*)/, method: :acknowledge_event
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,208 @@
1
+ module Zabbirc
2
+ module Irc
3
+ module PluginMethods
4
+
5
+ def acknowledge_event m, eventid, message
6
+ return unless authenticate m.user.nick
7
+ op = get_op m
8
+ event = find_event m, eventid
9
+ return unless event
10
+
11
+ if event.acknowledge "#{op.nick}: #{message}"
12
+ m.reply "#{op.nick}: Event `#{event.label}` acknowledged with message: #{message}"
13
+ else
14
+ m.reply "#{op.nick}: Could not acknowledge event `#{event.label}`"
15
+ end
16
+ end
17
+
18
+ def host_status m, host
19
+ return unless authenticate m.user.nick
20
+ op = get_op m
21
+ host = find_host m, host
22
+ return unless host
23
+
24
+ triggers = Zabbix::Trigger.get(hostids: host.id, filter: {value: 1}, selectHosts: :extend)
25
+ triggers = triggers.sort{|x,y| x.priority <=> y.priority }
26
+ msg = ["#{op.nick}: Host: #{host.name}"]
27
+ if triggers.empty?
28
+ msg[0] << " - status: OK"
29
+ else
30
+ msg[0] << " - status: #{triggers.size} problems"
31
+ triggers.each do |trigger|
32
+ msg << "#{op.nick}: status: #{trigger.label}"
33
+ end
34
+ end
35
+ m.reply msg.join("\n")
36
+ end
37
+
38
+ def host_latest m, host, _rest, limit
39
+ limit ||= 8
40
+ return unless authenticate m.user.nick
41
+ op = get_op m
42
+ host = find_host m, host
43
+ return unless host
44
+
45
+ msg = ["#{op.nick}: Host: #{host.name}"]
46
+ events = Zabbix::Event.get(hostids: host.id, limit: limit, selectHosts: :extend, selectRelatedObject: :extend, sortfield: :clock, sortorder: "DESC")
47
+ if events.empty?
48
+ msg[0] << " - no events found"
49
+ else
50
+ msg[0] << " - showing last #{events.size} events"
51
+ events.each do |event|
52
+ msg << "#{op.nick}: !latest: #{event.label}"
53
+ end
54
+ end
55
+ m.reply msg.join("\n")
56
+ end
57
+
58
+ def sync_ops m, u=nil
59
+ return if u and u.nick == bot.nick
60
+ bot.zabbirc_service.ops_service.iterate
61
+ end
62
+
63
+ ### Settings
64
+ def show_settings m
65
+ return unless authenticate m.user.nick
66
+ op = get_op m
67
+ m.reply "#{op.nick}: #{op.setting}"
68
+ end
69
+
70
+ def set_setting m, key, _rest, value
71
+ return unless authenticate m.user.nick
72
+ op = get_op m
73
+ case key
74
+ when "notify"
75
+ set_notify m, op, value
76
+ when "events_priority"
77
+ set_events_priority m, op, value
78
+ when "primary_channel"
79
+ set_primary_channel m, op, value
80
+ else
81
+ m.reply "#{op.nick}: unknown setting `#{key}`"
82
+ end
83
+ end
84
+
85
+ def set_notify m, op, value
86
+ if value.nil?
87
+ m.reply "#{op.nick}: notify allowed values: true, on, 1, false, off, 0"
88
+ return
89
+ end
90
+ case value
91
+ when "true", "on", "1"
92
+ op.setting.set :notify, true
93
+ when "false", "off", "0"
94
+ op.setting.set :notify, false
95
+ else
96
+ m.reply "#{op.nick}: uknown value `#{value}`. Allowed values: true, on, 1, false, off, 0"
97
+ return
98
+ end
99
+ m.reply "#{op.nick}: setting `notify` was set to `#{op.setting.get :notify}`"
100
+ end
101
+
102
+ def set_events_priority m, op, value
103
+ if value.nil?
104
+ m.reply "#{op.nick}: events_priority allowed values: #{Priority::PRIORITIES.values.collect{|v| "`#{v}`"}.join(', ')} or numeric #{Priority::PRIORITIES.keys.join(", ")} "
105
+ return
106
+ end
107
+ begin
108
+ value = value.to_i if value =~ /^\d+$/
109
+ priority = Priority.new value
110
+ rescue ArgumentError
111
+ m.reply "#{op.nick}: uknown value `#{value}`. Allowed values: #{Priority::PRIORITIES.values.collect{|v| "`#{v}`"}.join(', ')} or numeric #{Priority::PRIORITIES.keys.join(", ")} "
112
+ return
113
+ end
114
+ op.setting.set :events_priority, priority.code
115
+ m.reply "#{op.nick}: setting `events_priority` was set to `#{op.setting.get :events_priority}`"
116
+ end
117
+
118
+ def set_primary_channel m, op, value
119
+ channel_names = op.channels.collect(&:name)
120
+ if value.nil?
121
+ m.reply "#{op.nick}: notify allowed values: #{channel_names.join(", ")}"
122
+ return
123
+ end
124
+ case value
125
+ when *channel_names
126
+ op.setting.set :primary_channel, value
127
+ else
128
+ m.reply "#{op.nick}: uknown value `#{value}`. Allowed values: #{channel_names.join(", ")}"
129
+ return
130
+ end
131
+ m.reply "#{op.nick}: setting `primary_channel` was set to `#{op.setting.get :primary_channel}`"
132
+ end
133
+
134
+ ### Events
135
+ def list_events m
136
+ return unless authenticate m.user.nick
137
+ events = Zabbix::Event.recent
138
+ msg = if events.any?
139
+ events.collect do |e|
140
+ "#{m.user.nick}: #{e.label}"
141
+ end.join("\n")
142
+ else
143
+ "#{m.user.nick}: No last events"
144
+ end
145
+ m.reply msg
146
+ end
147
+
148
+ def ops
149
+ @ops ||= bot.zabbirc_service.ops
150
+ end
151
+
152
+ ### Authentication and helpers
153
+ def authenticate obj
154
+ nick = get_nick obj
155
+ ops.authenticate nick
156
+ end
157
+
158
+ def get_op obj
159
+ nick = get_nick obj
160
+ ops.get nick
161
+ end
162
+
163
+ def get_nick obj
164
+ case obj
165
+ when Cinch::Message
166
+ obj.user.nick
167
+ when Cinch::User
168
+ obj.nick
169
+ when String
170
+ obj
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def find_host m, host
177
+ op = get_op m
178
+ hosts = Zabbix::Host.get(search: {host: host})
179
+ case hosts.size
180
+ when 0
181
+ m.reply "#{op.nick}: Host not found `#{host}`"
182
+ when 1
183
+ return hosts.first
184
+ when 2..10
185
+ m.reply "#{op.nick}: Found #{hosts.size} hosts: #{hosts.collect(&:name).join(', ')}. Be more specific"
186
+ else
187
+ m.reply "#{op.nick}: Found #{hosts.size} Be more specific"
188
+ end
189
+ false
190
+ end
191
+
192
+ def find_event m, eventid
193
+ op = get_op m
194
+ begin
195
+ event = Zabbix::Event.find(eventid, {selectHosts: :extend, selectRelatedObject: :extend})
196
+ if event.nil?
197
+ m.reply "#{op.nick} Could not find event with id `#{eventid}`"
198
+ return false
199
+ end
200
+ event
201
+ rescue Zabbix::IDNotUniqueError => e
202
+ m.reply "#{op.nick} Could not find event: #{e}"
203
+ false
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,45 @@
1
+ module Zabbirc
2
+ def self.logger
3
+ @logger ||= ::Logger.new(STDERR, 10, 1.megabyte).tap do |logger|
4
+ logger.formatter = Zabbirc::Logger::Formatter.new
5
+ end
6
+ end
7
+
8
+ module Logger
9
+ class Formatter
10
+ Format = "%s, [%s#%d T%d] %5s -- %s: %s\n"
11
+
12
+ attr_accessor :datetime_format
13
+
14
+ def initialize
15
+ @datetime_format = nil
16
+ end
17
+
18
+ def call(severity, time, progname, msg)
19
+ Format % [severity[0..0], format_datetime(time), $$, Thread.current.object_id, severity, progname, msg2str(msg)]
20
+ end
21
+
22
+ private
23
+
24
+ def format_datetime(time)
25
+ if @datetime_format.nil?
26
+ time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d " % time.usec
27
+ else
28
+ time.strftime(@datetime_format)
29
+ end
30
+ end
31
+
32
+ def msg2str(msg)
33
+ case msg
34
+ when ::String
35
+ msg
36
+ when ::Exception
37
+ "#{ msg.message } (#{ msg.class })\n" <<
38
+ (msg.backtrace || []).join("\n")
39
+ else
40
+ msg.inspect
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/zabbirc/op.rb ADDED
@@ -0,0 +1,67 @@
1
+ module Zabbirc
2
+ class Op
3
+
4
+ attr_reader :channels, :setting, :nick
5
+ def initialize zabbix_user: nil, irc_user: nil
6
+ raise ArgumentError, 'zabbix_user' if zabbix_user.nil?
7
+ raise ArgumentError, 'irc_user' if irc_user.nil?
8
+ @nick = zabbix_user.alias
9
+ @zabbix_user = zabbix_user
10
+ @irc_user = irc_user
11
+
12
+ @notified_events = {}
13
+ @channels = Set.new
14
+ @setting = Setting.new
15
+ end
16
+
17
+ def primary_channel
18
+ @channels.find{|c| c.name == @setting.get(:primary_channel) }
19
+ end
20
+
21
+ def interesting_priority
22
+ Priority.new @setting.get(:events_priority)
23
+ end
24
+
25
+ def add_channel channel
26
+ @setting.fetch :primary_channel, channel
27
+ @channels << channel
28
+ end
29
+
30
+ def remove_channel channel
31
+ @channels.delete channel
32
+
33
+ if channel == @setting.get(:primary_channel)
34
+ @setting.set :primary_channel, @channels.first
35
+ end
36
+ end
37
+
38
+ def notify event
39
+ return if event.priority < interesting_priority
40
+ @notified_events ||= {}
41
+ return if @notified_events.key? event.id
42
+ channel = primary_channel
43
+ return unless channel
44
+ channel.send "#{@nick}: #{event.label}"
45
+ @notified_events[event.id] = Time.now
46
+ clear_notified_events
47
+ end
48
+
49
+ def interested_in? event
50
+ return false unless setting.get :notify
51
+ return false if @notified_events.key? event.id
52
+ event.priority >= interesting_priority
53
+ end
54
+
55
+ def event_notified event
56
+ @notified_events[event.id] = Time.now
57
+ end
58
+
59
+ private
60
+
61
+ def clear_notified_events
62
+ @notified_events.delete_if do |event_id, timestamp|
63
+ timestamp < (Zabbirc.config.notify_about_events_from_last * 2).seconds.ago
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ module Zabbirc
2
+ class OpList
3
+ include Enumerable
4
+
5
+ def initialize ops=nil
6
+ @ops = {}
7
+ if ops
8
+ ops.each do |op|
9
+ add op
10
+ end
11
+ end
12
+ end
13
+
14
+ def authenticate name
15
+ @ops.key? name
16
+ end
17
+
18
+ alias_method :exists?, :authenticate
19
+
20
+ def get name
21
+ @ops[name]
22
+ end
23
+
24
+ def add op
25
+ if exists? op.nick
26
+ return get(op.nick)
27
+ end
28
+ @ops[op.nick] = op
29
+ end
30
+
31
+ def each &block
32
+ @ops.values.each &block
33
+ end
34
+
35
+ def interested_in event
36
+ self.class.new(find_all{ |op| op.interested_in? event })
37
+ end
38
+
39
+ def notify event
40
+ group_by(&:primary_channel).each do |channel, ops|
41
+ op_targets = ops.collect{|op| "#{op.nick}:" }.join(" ")
42
+ channel.send "#{op_targets} #{event.label}"
43
+ ops.each{ |op| op.event_notified event }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ module Zabbirc
2
+ class Priority
3
+ include Comparable
4
+
5
+ PRIORITIES = {
6
+ 0 => :not_classified,
7
+ 1 => :information,
8
+ 2 => :warning,
9
+ 3 => :average,
10
+ 4 => :high,
11
+ 5 => :disaster
12
+ }
13
+
14
+ attr_reader :number, :code
15
+ def initialize priority
16
+ case priority
17
+ when String, Symbol
18
+ raise ArgumentError, "unknown priority `#{priority}`" unless PRIORITIES.key(priority.to_sym)
19
+ @number = PRIORITIES.key(priority.to_sym)
20
+ @code = priority.to_sym
21
+ when Integer
22
+ raise ArgumentError, "unknown priority `#{priority}`" unless PRIORITIES[priority]
23
+ @number = priority
24
+ @code = PRIORITIES[@number]
25
+ else
26
+ raise ArgumentError, "cannot create priority from `#{priority}` of class `#{priority.class}`"
27
+ end
28
+ end
29
+
30
+ def <=> other
31
+ number <=> other.number
32
+ end
33
+
34
+ def to_s
35
+ code.to_s
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,86 @@
1
+ require 'cinch'
2
+
3
+ module Zabbirc
4
+ class Service
5
+ attr_reader :cinch_bot
6
+ attr_reader :ops, :ops_service
7
+ attr_reader :running
8
+
9
+ def initialize
10
+ @semaphores_mutex = Mutex.new
11
+ @semaphores = Hash.new { |h, k| h[k] = Mutex.new }
12
+ @ops = OpList.new
13
+ @running = false
14
+
15
+ initialize_bot
16
+
17
+ @ops_service = Services::Ops.new self, @cinch_bot
18
+ @events_service = Services::Events.new self, @cinch_bot
19
+ end
20
+
21
+ def initialize_bot
22
+ @cinch_bot = Cinch::Bot.new do
23
+ configure do |c|
24
+ c.server = Zabbirc.config.irc_server
25
+ c.channels = Zabbirc.config.irc_channels
26
+ c.nick = "zabbirc"
27
+ c.plugins.plugins = [Irc::Plugin]
28
+ end
29
+ end
30
+
31
+ # Stores reference to this Zabbirc::Service to be available in plugins
32
+ @cinch_bot.instance_variable_set :@zabbirc_service, self
33
+ @cinch_bot.class_eval do
34
+ attr_reader :zabbirc_service
35
+ end
36
+ end
37
+
38
+ def synchronize(name, &block)
39
+ # Must run the default block +/ fetch in a thread safe way in order to
40
+ # ensure we always get the same mutex for a given name.
41
+ semaphore = @semaphores_mutex.synchronize { @semaphores[name] }
42
+ semaphore.synchronize(&block)
43
+ end
44
+
45
+ def start join=true
46
+ return if @running
47
+ @running = true
48
+ @cinch_bot_thread = Thread.new do
49
+ begin
50
+ @cinch_bot.start
51
+ rescue => e
52
+ Zabbirc.logger.fatal "Cinch bot start error: #{e}"
53
+ end
54
+ end
55
+
56
+ @controll_thread = Thread.new do
57
+ begin
58
+ sleep
59
+ rescue StopError
60
+ @ops_service.stop
61
+ @events_service.stop
62
+
63
+ Zabbirc.logger.info "Stopping ops service: #{@ops_service.join}"
64
+ Zabbirc.logger.info "Stopping events service: #{@events_service.join}"
65
+
66
+ @cinch_bot.quit
67
+ ensure
68
+ @running = false
69
+ end
70
+ end
71
+
72
+ @ops_service.start
73
+ @events_service.start
74
+
75
+ @cinch_bot_thread.join if join
76
+ end
77
+
78
+ def stop
79
+ @controll_thread.raise StopError
80
+ end
81
+
82
+ def join
83
+ @cinch_bot_thread.join
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,56 @@
1
+ module Zabbirc
2
+ module Services
3
+ class Base
4
+ LOOP_SLEEP = 10
5
+ attr_reader :running
6
+
7
+ def initialize service, cinch_bot
8
+ @service = service
9
+ @cinch_bot = cinch_bot
10
+ @running = false
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def synchronize &block
15
+ @mutex.synchronize &block
16
+ end
17
+
18
+ def join
19
+ slept = 0
20
+ while slept < (LOOP_SLEEP * 2)
21
+ return true if @running == false
22
+ sleep 1
23
+ slept += 1
24
+ end
25
+ false
26
+ end
27
+
28
+ def start
29
+ main_thread = Thread.current
30
+ @thread = Thread.new do
31
+ begin
32
+ loop do
33
+ Thread.handle_interrupt(StopError => :never) do
34
+ @running = true
35
+ iterate
36
+ end
37
+ Thread.handle_interrupt(StopError => :immediate) do
38
+ sleep LOOP_SLEEP
39
+ end
40
+ end
41
+ rescue StopError => e
42
+ # nothing, ensure block sets the variables
43
+ rescue => e
44
+ main_thread.raise e
45
+ ensure
46
+ @running = false
47
+ end
48
+ end
49
+ end
50
+
51
+ def stop
52
+ @thread.raise StopError
53
+ end
54
+ end
55
+ end
56
+ end