zmonitor 1.0.11

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.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'optparse'
4
+ require 'zmonitor'
5
+
6
+ opts = {}
7
+ OptionParser.new do |o|
8
+ o.banner = "usage: zmonitor [options]"
9
+ o.on('--ack MATCH', '-a', "Acknowledge current events that match a pattern MATCH. No wildcards.") { |a| opts[:ack] = a.tr('^ A-Za-z0-9[]{}()|,-.', '') }
10
+ o.on('--disable-maintenance', '-m', "Filter out servers marked as being in maintenance.") { |m| opts[:maint] = 1 }
11
+ o.on('--minimum-severity PRIORITY', '-M', "Show events with a priority greater than M. Accepted values are 0 to 5. Default is 2.") { |ms| opts[:min_severity] = ms.tr('^0-5', '') }
12
+ o.on('--priority-list LIST', '-l', "Comma-delimited list of what priority events to show.") { |l| opts[:priority_list] = l.tr('^,0-5', '') }
13
+ o.on('--hide-acknowledged-alerts', '-H', "Don't show events that have already been acknowledged.") { |h| opts[:hideack] = 1 }
14
+ o.on('--print-once', '-1', "Only check Zabbix once and print out all alerts.") { |p| opts[:once] = 1 }
15
+ o.on('-h', 'Show this help') { puts '',o,''; exit }
16
+ o.parse!
17
+ end
18
+
19
+ monitor = Zabbix::Monitor.new()
20
+ monitor.hide_maintenance = opts[:maint] unless opts[:maint].nil?
21
+ monitor.hide_acknowledged_alerts = opts[:hideack] unless opts[:hideack].nil?
22
+ monitor.min_severity = opts[:min_severity] unless opts[:min_severity].nil? and opts[:min_severity] != ''
23
+ monitor.priority_list = opts[:priority_list] unless opts[:priority_list].nil?
24
+
25
+ if opts[:ack]
26
+ monitor.acknowledge(opts[:ack])
27
+ elsif opts[:once]
28
+ monitor.get_dashboard('full')
29
+ else
30
+ system "stty -echo"
31
+ Signal.trap("SIGINT") do
32
+ system "stty echo"
33
+ abort
34
+ end
35
+ while true
36
+ monitor.get_dashboard()
37
+ 0.upto(20) { sleep 0.5 }
38
+ end
39
+ end
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'colored'
4
+
5
+ require 'zmonitor/api'
6
+ require 'zmonitor/misc'
7
+
8
+ module Zabbix
9
+ class Monitor
10
+ attr_accessor :api, :hide_maintenance, :hide_acknowledged_alerts, :min_severity, :priority_list
11
+
12
+ class EmptyFileError < StandardError
13
+ attr_reader :message
14
+ def initialize(reason, file)
15
+ @message = reason
16
+ puts "[INFO] Deleting #{file}"
17
+ File.delete(file)
18
+ end
19
+ end
20
+ def initialize()
21
+ @hide_maintenance = 0
22
+ @hide_acknowledged_alerts = 0
23
+ @min_severity = '2'
24
+ @priority_list = ''
25
+ uri = self.check_uri()
26
+ @api = Zabbix::API.new(uri)
27
+ @api.token = self.check_login
28
+ end
29
+ def check_uri()
30
+ uri_path = File.expand_path("~/.zmonitor-server")
31
+ if File.exists?(uri_path)
32
+ uri = File.open(uri_path).read()
33
+ else
34
+ puts "Where is your Zabbix located? (please include https/http - for example, https://localhost)"
35
+ uri = "#{STDIN.gets.chomp()}/api_jsonrpc.php"
36
+ f = File.new(uri_path, "w+")
37
+ f.write(uri)
38
+ f.close
39
+ end
40
+ #if uri !=~ /^https?:\/\/.*\/api_jsonrpc\.php/
41
+ #puts "The URI we're using is invalid, sir. Resetting..."
42
+ #check_uri()
43
+ #end
44
+ #puts "Okay, using #{uri}."
45
+ raise EmptyFileError.new('URI is empty for some reason', uri_path) if uri == '' || uri.nil?
46
+ return uri
47
+ end
48
+ def check_login()
49
+ token_path = File.expand_path("~/.zmonitor-token")
50
+ if File.exists?(token_path)
51
+ token = File.open(token_path).read()
52
+ else
53
+ print "Please enter your Zabbix username: "
54
+ user = STDIN.gets.chomp()
55
+ print "Please enter your Zabbix password: "
56
+ begin
57
+ system "stty -echo"
58
+ password = gets.chomp
59
+ ensure
60
+ system "stty echo"
61
+ puts
62
+ end
63
+ token = @api.user.login(user, password).chomp
64
+ f = File.new(token_path, "w+")
65
+ f.write(token)
66
+ f.close
67
+ end
68
+ raise EmptyFileError.new("Token is empty!", token_path) if token == '' || token.nil?
69
+ return token
70
+ end
71
+ def get_events()
72
+ current_time = Time.now.to_i # to be used in getting event durations, but it really depends on the master
73
+ triggers = unacked_triggers = @api.trigger.get_active(@min_severity, @hide_maintenance, @hide_acknowledged_alerts, @priority_list) # Call the API for a list of active triggers
74
+ unacked_triggers = @api.trigger.get_active(@min_severity, @hide_maintenance, 1, @priority_list) if @hide_acknowledged_alerts == 0 # Call it again to get just those that are unacknowledged
75
+ current_events = []
76
+ triggers.each do |t|
77
+ next if t['hosts'][0]['status'] == '1' or t['items'][0]['status'] == '1' # skip disabled items/hosts that the api call returns
78
+ current_events << {
79
+ :id => t['triggerid'].to_i,
80
+ :time => t['lastchange'].to_i,
81
+ :fuzzytime => fuzz(current_time - t['lastchange'].to_i),
82
+ :severity => t['priority'].to_i,
83
+ :hostname => t['host'],
84
+ :description => t['description'].gsub(/ (on(| server) |to |)#{t['host']}/, '')#,
85
+ }
86
+ end
87
+ current_events.each do |e|
88
+ s = unacked_triggers.select{ |t| t['triggerid'] == "#{e[:id]}" }
89
+ e[:acknowledged] = s[0] ? 0 : 1
90
+ end
91
+ # Sort the events decreasing by severity, and then descending by duration (smaller timestamps at top)
92
+ return current_events.sort_by { |t| [ -t[:severity], t[:time] ] }
93
+ end
94
+ def get_dashboard(format = '')
95
+ max_lines = `tput lines`.to_i - 1
96
+ cols = `tput cols`.to_i
97
+ eventlist = self.get_events() #TODO: get_events(max_lines)
98
+ pretty_output = []
99
+ pretty_output << ["Last updated: %8s%#{cols-40}sZmonitor Dashboard".cyan_on_blue % [Time.now.strftime('%T'),'']] if format != 'full'
100
+ if eventlist.length != 0
101
+ max_hostlen = eventlist.each.max { |a,b| a[:hostname].length <=> b[:hostname].length }[:hostname].length
102
+ max_desclen = eventlist.each.max { |a,b| a[:description].length <=> b[:description].length }[:description].length
103
+ eventlist.each do |e|
104
+ break if pretty_output.length == max_lines and format != 'full'
105
+ ack = "N".red
106
+ ack = "Y".green if e[:acknowledged] == 1
107
+ pretty_output << "%s " % e[:fuzzytime] + "%-#{max_hostlen}s " % e[:hostname] +
108
+ "%-#{max_desclen}s".color_by_severity(e[:severity]) % e[:description] + " %s" % ack
109
+ end
110
+ else
111
+ pretty_output << ['',
112
+ 'The API calls returned 0 results. Either your servers are very happy, or ZMonitor is not working correctly.',
113
+ '', "Please check your dashboard at #{@api.server.to_s.gsub(/\/api_jsonrpc.php/, '')} to verify activity.", '',
114
+ 'ZMonitor will continue to refresh every ten seconds unless you interrupt it.']
115
+ end
116
+ print "\e[H\e[2J" if format != 'full' # clear terminal screen
117
+ puts pretty_output
118
+ end
119
+ def acknowledge(pattern = '')
120
+ puts 'Retrieving list of active unacknowledged triggers that match: '.bold.blue + '%s'.green % pattern, ''
121
+ filtered = []
122
+ eventlist = self.get_events()
123
+ eventlist.each do |e|
124
+ if e[:hostname] =~ /#{pattern}/ or e[:description] =~ /#{pattern}/
125
+ event = @api.event.get_last_by_trigger(e[:id])
126
+ e[:eventid] = event['eventid'].to_i
127
+ e[:acknowledged] = event['acknowledged'].to_i
128
+ filtered << e if e[:acknowledged] == 0
129
+ end
130
+ end
131
+ abort("No alerts found, so aborting".yellow) if filtered.length == 0
132
+ filtered.each.with_index do |a,i|
133
+ message = '%s - %s (%s)'.color_by_severity(a[:severity]) % [ a[:fuzzytime], a[:description], a[:hostname] ]
134
+ puts "%4d >".bold % (i+1) + message
135
+ end
136
+
137
+ puts '', ' Selection - enter "all", or a set of numbers listed above separated by spaces.'
138
+ print ' Sel > '.bold
139
+ input = STDIN.gets.chomp()
140
+
141
+ no_ack_msg = "Not acknowledging anything."
142
+ raise StandardError.new("No input. #{no_ack_msg}".green) if input == ''
143
+ to_ack = (1..filtered.length).to_a if input =~ /^\s*all\s*$/ # only string we'll accept
144
+ raise StandardError.new("Invalid input. #{no_ack_msg}".red) if to_ack.nil? and (input =~ /^([0-9 ]+)$/).nil?
145
+ to_ack = input.split.map(&:to_i).sort if to_ack.nil? # Split our input into a sorted array of integers
146
+ # Let's first check if a value greater than possible was given, to help prevent typos acknowledging the wrong thing
147
+ to_ack.each { |i| raise StandardError.new("You entered a value greater than %d! Please double check. #{no_ack_msg}".yellow % filtered.length) if i > filtered.length }
148
+
149
+ puts '', ' Message - enter an acknowledgement message below, or leave blank for the default.'
150
+ print ' Msg > '.bold
151
+ message = STDIN.gets.chomp()
152
+ puts
153
+
154
+ # Finally! Acknowledge EVERYTHING
155
+ to_ack.each do |a|
156
+ puts 'Acknowledging: '.green + '%s (%s)' % [ filtered[a-1][:description], filtered[a-1][:hostname] ]
157
+ if message == ''
158
+ @api.whoami = @api.user.get_fullname()
159
+ @api.event.acknowledge(filtered[a-1][:eventid])
160
+ else
161
+ @api.event.acknowledge(filtered[a-1][:eventid], message)
162
+ end
163
+ end
164
+ end
165
+ # Save a time offset between the local computer and the Zabbix master
166
+ def calibrate()
167
+ #
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'net/https'
6
+
7
+ abort("Could not load API libraries. Did you install a JSON library? (json / json_pure / json-jruby)") unless Object.const_defined?(:JSON)
8
+
9
+ # create the module/class stub so we can require the API class files properly
10
+ module Zabbix
11
+ class API
12
+ end
13
+ end
14
+
15
+ # load up the different API classes and methods
16
+ require 'zmonitor/api/event'
17
+ require 'zmonitor/api/trigger'
18
+ require 'zmonitor/api/user'
19
+
20
+ module Zabbix
21
+ class API
22
+ attr_accessor :server, :verbose, :token, :whoami
23
+
24
+ attr_accessor :event, :trigger, :user # API classes
25
+
26
+ def initialize( server = "http://localhost", verbose = false)
27
+ # Parse the URL beforehand
28
+ @server = URI.parse(server)
29
+ @verbose = verbose
30
+
31
+ # set up API class methods
32
+ @user = Zabbix::User.new(self)
33
+ @event = Zabbix::Event.new(self)
34
+ @trigger = Zabbix::Trigger.new(self)
35
+ end
36
+
37
+ # More specific error names, may add extra handling procedures later
38
+ class ResponseCodeError < StandardError
39
+ end
40
+ class ResponseError < StandardError
41
+ end
42
+ class NotAuthorisedError < StandardError
43
+ end
44
+
45
+ def call_api(message)
46
+ # Finish preparing the JSON call
47
+ message['id'] = rand 65536 if message['id'].nil?
48
+ message['jsonrpc'] = '2.0'
49
+ # Check if we have authorization token if we're not logging in
50
+ if @token.nil? && message['method'] != 'user.login'
51
+ puts "[ERROR] Authorisation Token not initialised. message => #{message}"
52
+ raise NotAuthorisedError.new()
53
+ else
54
+ message['auth'] = @token if message['method'] != 'user.login'
55
+ end
56
+
57
+ json_message = JSON.generate(message) # Create a JSON string
58
+
59
+ # Open TCP connection to Zabbix master
60
+ connection = Net::HTTP.new(@server.host, @server.port)
61
+ connection.read_timeout = 300
62
+ # Check to see if we're connecting via SSL
63
+ if @server.scheme == 'https' then
64
+ connection.use_ssl = true
65
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
66
+ end
67
+
68
+ # Prepare POST request for sending
69
+ request = Net::HTTP::Post.new(@server.request_uri)
70
+ request.add_field('Content-Type', 'application/json-rpc')
71
+ request.body = json_message
72
+
73
+ # Send request
74
+ begin
75
+ puts "[INFO] Attempting to send request => #{request}, request body => #{request.body}" if @verbose
76
+ response = connection.request(request)
77
+ rescue ::SocketError => e
78
+ puts "[ERROR] Could not complete request: SocketError => #{e.message}" if @verbose
79
+ raise SocketError.new(e.message)
80
+ rescue Timeout::Error => e
81
+ puts "[ERROR] Timed out from Zabbix master. Is it being funky? => #{e.message}"
82
+ exit
83
+ end
84
+
85
+ puts "[INFO] Received response: #{response}" if @verbose
86
+ raise ResponseCodeError.new("[ERROR] Did not receive 200 OK, but HTTP code #{response.code}") if response.code != "200"
87
+
88
+ # Check for an error, and return the parsed result if everything's fine
89
+ parsed_response = JSON.parse(response.body)
90
+ if error = parsed_response['error']
91
+ raise ResponseError.new("[ERROR] Received error response: code => #{error['code'].to_s}; message => #{error['message']}; data => #{error['data']}")
92
+ end
93
+
94
+ return parsed_response['result']
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,39 @@
1
+ # api.event functions
2
+
3
+ module Zabbix
4
+ class Event < API
5
+ attr_accessor :parent
6
+ def initialize(parent)
7
+ @parent = parent
8
+ @verbose = @parent.verbose
9
+ end
10
+ def call_api(message)
11
+ return @parent.call_api(message)
12
+ end
13
+ # General event.get
14
+ def get( options = {} )
15
+ request = { 'method' => 'event.get', 'params' => options }
16
+ return call_api(request)
17
+ end
18
+ # Get the most recent event's information for a particular trigger
19
+ def get_last_by_trigger( triggerid = '' )
20
+ request = {
21
+ 'method' => 'event.get',
22
+ 'params' =>
23
+ {
24
+ 'triggerids' => [triggerid.to_s],
25
+ 'sortfield' => 'clock',
26
+ 'sortorder' => 'DESC',
27
+ 'limit' => '1',
28
+ 'output' => 'extend'
29
+ }
30
+ }
31
+ return call_api(request)[0]
32
+ end
33
+ # Mark an event acknowledged and leave a message
34
+ def acknowledge( events = [], message = "#{@parent.whoami} is working on this." )
35
+ request = { 'method' => 'event.acknowledge', 'params' => { 'eventids' => events, 'message' => message } }
36
+ call_api(request)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ # api.trigger functions
2
+
3
+ module Zabbix
4
+ class Trigger < API
5
+ attr_accessor :parent
6
+ def initialize(parent)
7
+ @parent = parent
8
+ @verbose = @parent.verbose
9
+ end
10
+ def call_api(message)
11
+ return @parent.call_api(message)
12
+ end
13
+ # General trigger.get
14
+ def get( options = {} )
15
+ request = { 'method' => 'trigger.get', 'params' => options }
16
+ return call_api(request)
17
+ end
18
+ # Get a hash of all unresolved problem triggers
19
+ def get_active( min_severity = 2, maint = 0, lastack = 0, priority_list = '' )
20
+ request = {
21
+ 'method' => 'trigger.get',
22
+ 'params' => {
23
+ 'sortfield' => 'priority,lastchange',
24
+ 'sortorder' => 'desc',
25
+ 'templated' => '0',
26
+ 'filter' => { 'value' => '1', 'status' => '0' },
27
+ 'expandData' => 'host',
28
+ 'expandDescription' => '1',
29
+ 'select_hosts' => 'extend',
30
+ 'select_items' => 'extend',
31
+ 'output' => 'extend'
32
+ }
33
+ }
34
+ request['params']['maintenance'] = 0 if maint == 1
35
+ request['params']['withLastEventUnacknowledged'] = 1 if lastack == 1
36
+ if priority_list == ''
37
+ request['params']['min_severity'] = min_severity.to_s
38
+ else
39
+ request['params']['filter']['priority'] = priority_list.split(",")
40
+ end
41
+ return call_api(request)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # api.user functions
2
+
3
+ module Zabbix
4
+ class User < API
5
+ attr_accessor :parent
6
+ def initialize(parent)
7
+ @parent = parent
8
+ @verbose = @parent.verbose
9
+ end
10
+ def call_api(message)
11
+ return @parent.call_api(message)
12
+ end
13
+ # General user.get
14
+ def get( options = {} )
15
+ request = { 'method' => 'user.get', 'params' => options }
16
+ return call_api(request)
17
+ end
18
+ # Get first and last name of currently logged in user
19
+ def get_fullname()
20
+ request = { 'method' => 'user.get', 'output' => 'extend' }
21
+ whoami = self.get({ 'output' => 'extend' })
22
+ return whoami[0]["name"] + " " + whoami[0]["surname"]
23
+ end
24
+ # Perform a login procedure
25
+ def login(user, password)
26
+ request = { 'method' => 'user.login', 'params' => { 'user' => user, 'password' => password, }, 'id' => 1 }
27
+ puts "[INFO] Logging in..." if @verbose
28
+ result = call_api(request)
29
+ puts "[INFO] Successfully logged in as #{user}! result => #{result}" if @verbose
30
+ return result
31
+ end
32
+ # Perform a logout
33
+ def logout()
34
+ request = { 'method' => 'user.logout' }
35
+ puts "[INFO] Logging out..." if @verbose
36
+ call_api(request)
37
+ puts "[INFO] Successfully logged out." if @verbose
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # Miscellaneous functions that aren't technically part of Zabbix, but are used in zabbixmon
4
+
5
+ def fuzz(t)
6
+ t = 0 if t < 0 # we don't need negative fuzzy times.
7
+ d = t / 86400
8
+ h = t % 86400 / 3600
9
+ m = t % 3600 / 60
10
+ s = t % 60
11
+ fuzzy = ['d', 'h', 'm', 's'].map do |unit|
12
+ amt = eval(unit)
13
+ "%3d#{unit}" % amt
14
+ end.join
15
+ return "#{fuzzy}"[8..-1] if h == 0
16
+ return "#{fuzzy}"[4..-5] if d == 0
17
+ return "#{fuzzy}"[0..-9]
18
+ end
19
+
20
+ class String
21
+ def color_by_severity( level = 0 )
22
+ case level
23
+ when 5; self.bold.red
24
+ when 4; self.yellow
25
+ when 3; self.green
26
+ when 2; self.cyan
27
+ when 1; self.bold.white
28
+ else self
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zmonitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.11
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Musee Ullah
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: colored
16
+ requirement: &19182760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *19182760
25
+ description: A command line interface for viewing alerts from a Zabbix instance.
26
+ email: milkteafuzz@gmail.com
27
+ executables:
28
+ - zmonitor
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - lib/zmonitor/api.rb
33
+ - lib/zmonitor/misc.rb
34
+ - lib/zmonitor/api/user.rb
35
+ - lib/zmonitor/api/event.rb
36
+ - lib/zmonitor/api/trigger.rb
37
+ - lib/zmonitor.rb
38
+ - bin/zmonitor
39
+ homepage: https://github.com/liliff/zonitor
40
+ licenses: []
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements:
58
+ - json
59
+ rubyforge_project:
60
+ rubygems_version: 1.8.17
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: Zabbix CLI dashboard
64
+ test_files: []