zmonitor 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []