rubix 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ module Rubix
2
+
3
+ class Template < Model
4
+
5
+ attr_accessor :name, :host_ids
6
+
7
+ def initialize properties={}
8
+ super(properties)
9
+ @name = properties[:name]
10
+ end
11
+
12
+ def log_name
13
+ "TEMPLATE #{name || id}"
14
+ end
15
+
16
+ def register
17
+ exists? ? update : create
18
+ end
19
+
20
+ def unregister
21
+ destroy if exists?
22
+ end
23
+
24
+ def load
25
+ response = request('template.get', 'filter' => {'templateid' => id, 'name' => name}, 'select_hosts' => 'refer', 'output' => 'extend')
26
+ case
27
+ when response.has_data?
28
+ @id = response.first['templateid'].to_i
29
+ @name = response.first['name']
30
+ @host_ids = response.first['hosts'].map { |host_info| host_info['hostid'].to_i }
31
+ @loaded = true
32
+ @exists = true
33
+ when response.success?
34
+ @exists = false
35
+ @loaded = true
36
+ else
37
+ error("Could not load: #{response.error_messaage}")
38
+ end
39
+ end
40
+
41
+ def create
42
+ response = request('template.create', [{'name' => name}])
43
+ if response.has_data?
44
+ @id = response['templateids'].first.to_i
45
+ @exists = true
46
+ info("Created")
47
+ else
48
+ error("Could not create: #{response.error_message}.")
49
+ end
50
+ end
51
+
52
+ def update
53
+ # noop
54
+ info("Updated")
55
+ end
56
+
57
+ def destroy
58
+ response = request('template.delete', [{'templateid' => id}])
59
+ case
60
+ when response.has_data? && response['templateids'].first.to_i == id
61
+ info("Deleted")
62
+ when response.zabbix_error? && response.error_message =~ /does not exist/i
63
+ # was never there...
64
+ else
65
+ error("Could not delete: #{response.error_message}")
66
+ end
67
+ end
68
+
69
+ def contains? host
70
+ return unless exists?
71
+ host_ids.include?(host.id)
72
+ end
73
+
74
+ def self.find_or_create_by_name name
75
+ new(:name => name).tap do |group|
76
+ group.create unless group.exists?
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,167 @@
1
+ require 'configliere'
2
+ require 'json'
3
+
4
+ module Rubix
5
+
6
+ # A generic monitor class for constructing Zabbix monitors.
7
+ #
8
+ # This class handles the low-level logic of sleeping, waking up, and
9
+ # sending data to Zabbix.
10
+ #
11
+ # It's up to a subclass to determine how to make a measurement.
12
+ #
13
+ # Here's an example of a script which measures the uptime of the
14
+ # current machine.
15
+ #
16
+ # #!/usr/bin/env ruby
17
+ # # in uptime_monitor
18
+ # class UptimeMonitor < Rubix::Monitor
19
+ #
20
+ # def measure
21
+ # return unless `uptime`.chomp =~ /(\d+) days/
22
+ # write do |data|
23
+ # data << ([['uptime', $1.to_i]])
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # UptimeMonitor.run if $0 == __FILE__
29
+ #
30
+ # See what the script measures by running it directly.
31
+ #
32
+ # $ ./uptime_monitor
33
+ #
34
+ # Or have it send its output to another file or FIFO
35
+ #
36
+ # $ ./uptime_monitor /path/to/some/file
37
+ #
38
+ # Or have it loop every 30 seconds
39
+ #
40
+ # $ ./uptime_monitor --loop=30 /path/to/some/file &
41
+ class Monitor
42
+
43
+ #
44
+ # Class-level settings and a function to run a monito
45
+ #
46
+
47
+ def self.default_settings
48
+ Configliere::Param.new.tap do |s|
49
+ s.use :commandline
50
+
51
+ s.define :loop, :description => "Run every this many seconds", :required => false, :type => Integer
52
+ end
53
+ end
54
+
55
+ def self.run
56
+ settings = default_settings
57
+ begin
58
+ settings.resolve!
59
+ rescue => e
60
+ puts e.message
61
+ exit(1)
62
+ end
63
+ new(settings).run
64
+ end
65
+
66
+ #
67
+ # Instance-level settings that provide logic for running once or
68
+ # looping.
69
+ #
70
+
71
+ attr_reader :settings
72
+
73
+ def initialize settings
74
+ @settings = settings
75
+ end
76
+
77
+ def loop?
78
+ loop_period > 0
79
+ end
80
+
81
+ def loop_period
82
+ settings[:loop].to_i
83
+ end
84
+
85
+ def run
86
+ begin
87
+ if loop?
88
+ while true
89
+ measure
90
+ output.flush if output
91
+ sleep loop_period
92
+ end
93
+ else
94
+ measure
95
+ end
96
+ ensure
97
+ close
98
+ end
99
+ end
100
+
101
+ def measure
102
+ raise NotImplementedError.new("Override the 'measure' method in a subclass to conduct a measurement.")
103
+ end
104
+
105
+ #
106
+ # Methods for writing data to Zabbix.
107
+ #
108
+
109
+ def write options={}, &block
110
+ return unless output
111
+ data = []
112
+ block.call(data) if block_given?
113
+ text = {
114
+ :data => data.map do |measurement|
115
+ key, value = measurement
116
+ { :key => key, :value => value }
117
+ end
118
+ }.merge(options).to_json
119
+
120
+ begin
121
+ output.puts(text)
122
+ rescue Errno::ENXIO
123
+ # FIFO's reader isn't alive...
124
+ end
125
+ end
126
+
127
+ def output_path
128
+ settings.rest.first
129
+ end
130
+
131
+ def stdout?
132
+ output_path.nil?
133
+ end
134
+
135
+ def file?
136
+ !stdout? && (!File.exist?(output_path) || File.ftype(output_path) == 'file')
137
+ end
138
+
139
+ def fifo?
140
+ !stdout? && File.exist?(output_path) && File.ftype(output_path) == 'fifo'
141
+ end
142
+
143
+ def output
144
+ return @output if @output
145
+ case
146
+ when stdout?
147
+ @output = $stdout
148
+ when fifo?
149
+ begin
150
+ @output = open(output_path, (File::WRONLY | File::NONBLOCK))
151
+ rescue Errno::ENXIO
152
+ # FIFO's reader isn't alive...
153
+ end
154
+ else
155
+ @output = File.open(output_path, 'a')
156
+ end
157
+ end
158
+
159
+ def close
160
+ return unless output
161
+ output.flush
162
+ return if stdout?
163
+ output.close
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,82 @@
1
+ module Rubix
2
+ # A generic monitor class for constructing Zabbix monitors that need
3
+ # to talk to Chef servers.
4
+ #
5
+ # This class handles the low-level logic of connecting to Chef and
6
+ # parsing results from searches.
7
+ #
8
+ # It's still up to a subclass to determine how to make a measurement.
9
+ #
10
+ # Here's an example of a script which checks the availibility of a web
11
+ # server at the EC2 public hostname of the Chef node 'webserver'.
12
+ #
13
+ # #!/usr/bin/env ruby
14
+ # # in webserver_monitor
15
+ #
16
+ # require 'net/http'
17
+ #
18
+ # class WebserverMonitor < Rubix::ChefMonitor
19
+ #
20
+ # def measure
21
+ # webserver = chef_node_from_node_name('webserver')
22
+ # begin
23
+ # if Net::HTTP.get_response(URI.parse("http://#{webserver['ec2']['public_hostname']}")).code.to_i == 200
24
+ # write do |data|
25
+ # data << ['webserver.available', 1]
26
+ # end
27
+ # return
28
+ # end
29
+ # rescue => e
30
+ # end
31
+ # write do |data|
32
+ # data << ([['webserver.available', 0]])
33
+ # end
34
+ # end
35
+ # end
36
+ #
37
+ # WebserverMonitor.run if $0 == __FILE__
38
+ #
39
+ # See documentation for Rubix::Monitor to understand how to run this
40
+ # script.
41
+ class ChefMonitor < Monitor
42
+
43
+ def self.default_settings
44
+ super().tap do |s|
45
+ s.define :chef_server_url, :description => "Chef server URL" , :required => true
46
+ s.define :chef_node_name, :description => "Node name to identify to Chef server", :required => true
47
+ s.define :chef_client_key, :description => "Path to Chef client private key", :required => true
48
+ end
49
+ end
50
+
51
+ def initialize settings
52
+ super(settings)
53
+ set_chef_credentials
54
+ end
55
+
56
+ def set_chef_credentials
57
+ require 'chef'
58
+ Chef::Config[:chef_server_url] = settings[:chef_server_url]
59
+ Chef::Config[:node_name] = settings[:chef_node_name]
60
+ Chef::Config[:client_key] = settings[:chef_client_key]
61
+ end
62
+
63
+ def search_nodes *args
64
+ Chef::Search::Query.new.search('node', *args)
65
+ end
66
+
67
+ def chef_node_from_node_name node_name
68
+ return if node_name.nil? || node_name.empty?
69
+ results = search_nodes("name:#{node_name}")
70
+ return unless results.first.size > 0
71
+ results.first.first
72
+ end
73
+
74
+ def chef_node_name_from_ip ip
75
+ return if ip.nil? || ip.empty?
76
+ results = search_nodes("ipaddress:#{ip} OR fqdn:#{ip}")
77
+ return unless results.first.size > 0
78
+ results.first.first['node_name']
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,84 @@
1
+ module Rubix
2
+
3
+ # A generic monitor class for constructing Zabbix monitors that
4
+ # monitor whole clusters.
5
+ #
6
+ # This class handles the low-level logic of finding a set of nodes and
7
+ # then grouping them by cluster.
8
+ #
9
+ # It's still up to a subclass to determine how to make a measurement
10
+ # on the cluster.
11
+ #
12
+ # Here's an example of a script which finds the average uptime of
13
+ # nodes a value of 'bar' set for property 'foo', grouped by cluster.
14
+ #
15
+ # #!/usr/bin/env ruby
16
+ # # in cluster_uptime_monitor
17
+ #
18
+ # class ClusterUptimeMonitor < Rubix::ClusterMonitor
19
+ #
20
+ # def node_query
21
+ # 'role:nginx'
22
+ # end
23
+ #
24
+ # def measure_cluster cluster_name
25
+ # total_seconds = nodes_by_cluster[cluster_name].inject(0.0) do |sum, node|
26
+ # sum += node['uptime_seconds']
27
+ # end
28
+ # average_uptime = total_seconds.to_f / nodes_by_cluster[cluster_name].size.to_f
29
+ # write(:hostname => 'cluster_name') do |data|
30
+ # data << ['uptime.average', average_uptime]
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ # ClusterUptimeMonitor.run if $0 == __FILE__
36
+ #
37
+ # See documentation for Rubix::Monitor to understand how to run this
38
+ # script.
39
+ class ClusterMonitor < ChefMonitor
40
+
41
+ attr_reader :private_ips_by_cluster, :nodes_by_cluster
42
+
43
+ def initialize settings
44
+ super(settings)
45
+ group_nodes_by_cluster
46
+ end
47
+
48
+ def node_query
49
+ ''
50
+ end
51
+
52
+ def matching_chef_nodes
53
+ search_nodes(node_query)
54
+ end
55
+
56
+ def group_nodes_by_cluster
57
+ @private_ips_by_cluster = {}
58
+ @nodes_by_cluster = {}
59
+ matching_chef_nodes.first.each do |node|
60
+ @nodes_by_cluster[node['cluster_name']] ||= []
61
+ @nodes_by_cluster[node['cluster_name']] << node
62
+
63
+ @private_ips_by_cluster[node['cluster_name']] ||= []
64
+ @private_ips_by_cluster[node['cluster_name']] << node['ipaddress']
65
+ end
66
+ end
67
+
68
+ def clusters
69
+ private_ips_by_cluster.keys
70
+ end
71
+
72
+ def measure
73
+ clusters.each do |cluster_name|
74
+ measure_cluster(cluster_name)
75
+ end
76
+ end
77
+
78
+ def measure_cluster cluster_name
79
+ raise NotImplementedError.new("Override the 'measure_cluster' method to make measurements of a given cluster.")
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,124 @@
1
+ require 'json'
2
+
3
+ module Rubix
4
+
5
+ class Response
6
+
7
+ attr_reader :http_response, :code, :body
8
+
9
+ def initialize(http_response)
10
+ @http_response = http_response
11
+ @body = http_response.body
12
+ @code = http_response.code.to_i
13
+ end
14
+
15
+ #
16
+ # Parsing
17
+ #
18
+
19
+ def parsed
20
+ return @parsed if @parsed
21
+ if non_200?
22
+ @parsed = {}
23
+ else
24
+ begin
25
+ @parsed = JSON.parse(@body) if @code == 200
26
+ rescue JSON::ParserError => e
27
+ @parsed = {}
28
+ end
29
+ end
30
+ end
31
+
32
+ #
33
+ # Error Handling
34
+ #
35
+
36
+ def non_200?
37
+ code != 200
38
+ end
39
+
40
+ def error?
41
+ non_200? || (parsed.is_a?(Hash) && parsed['error'])
42
+ end
43
+
44
+ def zabbix_error?
45
+ code == 200 && error?
46
+ end
47
+
48
+ def error_code
49
+ return unless error?
50
+ (non_200? ? code : parsed['error']['code'].to_i) rescue 0
51
+ end
52
+
53
+ def error_type
54
+ return unless error?
55
+ (non_200? ? "Non-200 Error" : parsed['error']['message']) rescue 'Unknown Error'
56
+ end
57
+
58
+ def error_message
59
+ return unless error?
60
+ begin
61
+ if non_200?
62
+ "Could not get a 200 response from the Zabbix API. Further details are unavailable."
63
+ else
64
+ stripped_message = (parsed['error']['message'] || '').gsub(/\.$/, '')
65
+ stripped_data = (parsed['error']['data'] || '').gsub(/^\[.*?\] /, '')
66
+ [stripped_message, stripped_data].map(&:strip).reject(&:empty?).join(', ')
67
+ end
68
+ rescue => e
69
+ "No details available."
70
+ end
71
+ end
72
+
73
+ def success?
74
+ !error?
75
+ end
76
+
77
+ #
78
+ # Inspecting contents
79
+ #
80
+
81
+ def result
82
+ parsed['result']
83
+ end
84
+
85
+ def [] key
86
+ return if error?
87
+ result[key]
88
+ end
89
+
90
+ def first
91
+ return if error?
92
+ result.first
93
+ end
94
+
95
+ def empty?
96
+ result.empty?
97
+ end
98
+
99
+ def has_data?
100
+ success? && (!empty?)
101
+ end
102
+
103
+ def hash?
104
+ return false if error?
105
+ result.is_a?(Hash) && result.size > 0 && result.first.last
106
+ end
107
+
108
+ def array?
109
+ return false if error?
110
+ result.is_a?(Array) && result.size > 0 && result.first
111
+ end
112
+
113
+ def string?
114
+ return false if error?
115
+ result.is_a?(String) && result.size > 0
116
+ end
117
+
118
+ def boolean?
119
+ return false if error?
120
+ result == true || result == false
121
+ end
122
+
123
+ end
124
+ end