rubix 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.4.1
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'cgi'
2
3
  require 'net/http'
3
4
  require 'json'
4
5
 
@@ -11,6 +12,13 @@ module Rubix
11
12
 
12
13
  include Logs
13
14
 
15
+ # The name of the cookie used by the Zabbix web application. Used
16
+ # when emulating a request from a browser.
17
+ COOKIE_NAME = 'zbx_sessionid'
18
+
19
+ # The content type header to send when emulating a browser.
20
+ CONTENT_TYPE = 'multipart/form-data'
21
+
14
22
  # @return [URI] The URI for the Zabbix API.
15
23
  attr_reader :uri
16
24
 
@@ -62,18 +70,33 @@ module Rubix
62
70
  #
63
71
  # @param [String] method the name of the Zabbix API method
64
72
  # @param [Hash,Array] params parameters for the method call
65
- # @retrn [Rubix::Response]
73
+ # @return [Rubix::Response]
66
74
  def request method, params
75
+ authorize! unless authorized?
76
+ response = till_response do
77
+ send_api_request :jsonrpc => "2.0",
78
+ :id => request_id,
79
+ :method => method,
80
+ :params => params,
81
+ :auth => auth
82
+ end
83
+ Response.new(response)
84
+ end
85
+
86
+ # Send a request to the Zabbix web application. The request is
87
+ # designed to emulate a web browser.
88
+ #
89
+ # Any values in +data+ which are file handles will trigger a
90
+ # multipart POST request, uploading those files.
91
+ #
92
+ # @param [String] verb one of "GET" or "POST"
93
+ # @param [String] path the path to send the request to
94
+ # @param [Hash] data the data to include in the request
95
+ # @return [Net::HTTP::Response]
96
+ def web_request verb, path, data={}
67
97
  authorize! unless authorized?
68
98
  till_response do
69
- raw_params = {
70
- :jsonrpc => "2.0",
71
- :id => request_id,
72
- :method => method,
73
- :params => params,
74
- :auth => auth
75
- }
76
- send_raw_request(raw_params)
99
+ send_web_request(verb, path, data)
77
100
  end
78
101
  end
79
102
 
@@ -86,7 +109,7 @@ module Rubix
86
109
  # Force the connection to execute an authorization request and
87
110
  # renew (or set) the authorization token.
88
111
  def authorize!
89
- response = till_response { send_raw_request(authorization_params) }
112
+ response = Response.new(till_response { send_api_request(authorization_params) })
90
113
  raise AuthenticationError.new("Could not authenticate with Zabbix API at #{uri}: #{response.error_message}") if response.error?
91
114
  raise AuthenticationError.new("Malformed response from Zabbix API: #{response.body}") unless response.string?
92
115
  @auth = response.result
@@ -145,7 +168,7 @@ module Rubix
145
168
  when response.code.to_i >= 500
146
169
  raise ConnectionError.new("Too many consecutive failed requests (#{max_attempts}) to the Zabbix API at (#{uri}).")
147
170
  else
148
- @last_response = Response.new(response)
171
+ @last_response = response
149
172
  end
150
173
  end
151
174
 
@@ -153,21 +176,42 @@ module Rubix
153
176
  #
154
177
  # @param [Hash, #to_json] raw_params the complete parameters of the request.
155
178
  # @return [Net::HTTP::Response]
156
- def send_raw_request raw_params
179
+ def send_api_request raw_params
157
180
  @request_id += 1
158
181
  begin
159
- raw_response = server.request(raw_post_request(raw_params))
182
+ raw_response = server.request(raw_api_request(raw_params))
160
183
  rescue NoMethodError, SocketError => e
161
184
  raise RequestError.new("Could not connect to Zabbix server at #{host_with_port}")
162
185
  end
163
186
  raw_response
164
187
  end
188
+
189
+ # Send a Web request to Zabbix.
190
+ #
191
+ # The existing authorization token will be used to emulate a
192
+ # request sent by a browser.
193
+ #
194
+ # Any values in +data+ which are file handles will trigger a
195
+ # multipart POST request, uploading those files.
196
+ #
197
+ # @param [String] verb one of "GET" or "POST"
198
+ # @param [String] path the path to send the request to
199
+ # @param [Hash] data the data to include in the request
200
+ def send_web_request verb, path, data={}
201
+ # Don't increment this for web requests?
202
+ # @request_id += 1
203
+ begin
204
+ raw_response = server.request(raw_web_request(verb, path, data))
205
+ rescue NoMethodError, SocketError => e
206
+ raise RequestError.new("Could not connect to the Zabbix server at #{host_with_port}")
207
+ end
208
+ end
165
209
 
166
210
  # Generate the raw POST request to send to the Zabbix API
167
211
  #
168
212
  # @param [Hash, #to_json] raw_params the complete parameters of the request.
169
213
  # @return [Net::HTTP::Post]
170
- def raw_post_request raw_params
214
+ def raw_api_request raw_params
171
215
  json_body = raw_params.to_json
172
216
  Rubix.logger.log(Logger::DEBUG, "SEND: #{json_body}") if Rubix.logger
173
217
  Net::HTTP::Post.new(uri.path).tap do |req|
@@ -176,6 +220,94 @@ module Rubix
176
220
  end
177
221
  end
178
222
 
223
+ # Generate a raw web request to send to the Zabbix web application
224
+ # as though it came from a browser.
225
+ #
226
+ # @param [String] verb the HTTP verb, either "GET" (default) or "POST"
227
+ # @param [String] path the path on the server to send the request to
228
+ # @param [Hash] data the data for the request
229
+ def raw_web_request verb, path, data={}
230
+ case
231
+ when verb == "GET"
232
+ raw_get_request(path)
233
+ when verb == "POST" && data.values.any? { |value| value.respond_to?(:read) }
234
+ raw_multipart_post_request(path, data)
235
+ when verb == "POST"
236
+ raw_post_request(path, data)
237
+ else
238
+ raise Rubix::RequestError.new("Invalid HTTP verb: #{verb}")
239
+ end
240
+ end
241
+
242
+ # Generate an authenticated GET request emulating a browser.
243
+ #
244
+ # @param [String] path the path to send the request to.
245
+ # @return [Net::HTTP::Get]
246
+ def raw_get_request(path)
247
+ Net::HTTP::Get.new(path).tap do |req|
248
+ req['Content-Type'] = self.class::CONTENT_TYPE
249
+ req['Cookie'] = "#{self.class::COOKIE_NAME}=#{CGI::escape(auth.to_s)}"
250
+ end
251
+ end
252
+
253
+ # Generate an authenticated POST request emulating a browser. It
254
+ # is assumed that +data+ is not multipart data.
255
+ #
256
+ # @param [String] path the path to send the request to.
257
+ # @param [Hash] data the data to send
258
+ # @return [Net::HTTP::Post]
259
+ def raw_post_request(path, data={})
260
+ Net::HTTP::Post.new(path).tap do |req|
261
+ req['Content-Type'] = self.class::CONTENT_TYPE
262
+ req['Cookie'] = "#{self.class::COOKIE_NAME}=#{CGI::escape(auth.to_s)}"
263
+ req.body = formatted_post_body(data)
264
+ end
265
+ end
266
+
267
+ # Generate an authenticated POST request emulating a browser.
268
+ # Assumes data is multipart data, with some values being file
269
+ # handles.
270
+ #
271
+ # @param [String] path the path to send the request to.
272
+ # @param [Hash] data the data to send
273
+ # @return [Net::HTTP::Post::Multipart]
274
+ def raw_multipart_post_request(path, data={})
275
+ require 'net/http/post/multipart'
276
+ Net::HTTP::Post::Multipart.new(path, wrapped_multipart_post_data(data)).tap do |req|
277
+ req['Cookie'] = "#{self.class::COOKIE_NAME}=#{CGI::escape(auth.to_s)}"
278
+ end
279
+ end
280
+
281
+ # Wrap +data+ with +UploadIO+ objects so that it can be properly
282
+ # handled by the Net::HTTP::Post::Multipart class.
283
+ #
284
+ # @param [Hash] data
285
+ # @return [Hash]
286
+ def wrapped_multipart_post_data data
287
+ {}.tap do |wrapped|
288
+ data.each_pair do |key, value|
289
+ if value.respond_to?(:read)
290
+ # We are going to assume it's always XML we're uploading.
291
+ wrapped[key] = UploadIO.new(value, "application/xml", File.basename(value.path))
292
+ else
293
+ wrapped[key] = value
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ # Format +data+ as a POST data string.
300
+ #
301
+ # @param [Hash] data
302
+ # @return [String]
303
+ def formatted_post_body data
304
+ [].tap do |pairs|
305
+ data.each_pair do |key, value|
306
+ pairs << [key, value].map { |s| CGI::escape(s.to_s) }.join('=')
307
+ end
308
+ end.join('&')
309
+ end
310
+
179
311
  # Used for generating helpful error messages.
180
312
  #
181
313
  # @return [String]
@@ -101,6 +101,7 @@ module Rubix
101
101
  hp[:port] = port || self.class::DEFAULT_PORT
102
102
  else
103
103
  hp[:ip] = self.class::BLANK_IP
104
+ hp[:useip] = 1
104
105
  end
105
106
  end
106
107
  end
@@ -93,6 +93,16 @@ module Rubix
93
93
  Rubix.connection && Rubix.connection.request(method, params)
94
94
  end
95
95
 
96
+ # Send a web request to the Zabbix web application. This is just
97
+ # a convenience method for <tt>Rubix::Connection#web_request</tt>.
98
+ #
99
+ # @param [String] verb one of "GET" or "POST"
100
+ # @param [String] path the path to send the request to
101
+ # @param [Hash] data the data to include with the request
102
+ def self.web_request verb, path, data={}
103
+ Rubix.connection && Rubix.connection.web_request(verb, path, data)
104
+ end
105
+
96
106
  # Is this a new record? We can tell because the ID must be blank.
97
107
  #
98
108
  # @return [true, false]
@@ -67,6 +67,48 @@ module Rubix
67
67
  :host_group_ids => template['groups'].map { |group| group['groupid'].to_i }
68
68
  })
69
69
  end
70
+
71
+ #
72
+ # == Import/Export ==
73
+ #
74
+
75
+ # Options which control the template import process and the Zabbix
76
+ # keys they need to be mapped to.
77
+ IMPORT_OPTIONS = {
78
+ :update_hosts => 'rules[host][exist]',
79
+ :add_hosts => 'rules[host][missed]',
80
+ :update_items => 'rules[item][exist]',
81
+ :add_items => 'rules[item][missed]',
82
+ :update_triggers => 'rules[trigger][exist]',
83
+ :add_triggers => 'rules[trigger][missed]',
84
+ :update_graphs => 'rules[graph][exist]',
85
+ :add_graphs => 'rules[graph][missed]',
86
+ :update_templates => 'rules[template][exist]'
87
+ }.freeze
88
+
89
+ # Import/update a template from XML contained in an open file
90
+ # handle +fh+.
91
+ #
92
+ # By default all hosts, items, triggers, and graphs the XML
93
+ # defines will be both added and updated. (Linked templates will
94
+ # also be updated.) This behavior matches the default behavior of
95
+ # the web interface in Zabbix 1.8.8.
96
+ #
97
+ # This can be controlled with options like <tt>:update_hosts</tt>
98
+ # or <tt>:add_graphs</tt>, all of which default to true. (Linked
99
+ # templates are controlled with <tt>:update_templates</tt>.)
100
+ def self.import fh, options={}
101
+ response = web_request("POST", "/templates.php", import_options(options).merge(:import_file => fh))
102
+ File.open('/tmp/output.html', 'w') { |f| f.puts(response.body) }
103
+ end
104
+
105
+ def self.import_options options
106
+ {}.tap do |o|
107
+ self::IMPORT_OPTIONS.each_pair do |name, zabbix_name|
108
+ o[zabbix_name] = 'yes' unless options[name] == false
109
+ end
110
+ end
111
+ end
70
112
 
71
113
  end
72
114
  end
@@ -13,6 +13,7 @@ module Rubix
13
13
  super(properties)
14
14
  @from = properties[:from]
15
15
  @upto = properties[:upto]
16
+ @raw_data = properties[:raw_data]
16
17
 
17
18
  self.item = properties[:item]
18
19
  self.item_id = properties[:item_id]
@@ -37,6 +38,14 @@ module Rubix
37
38
  def upto
38
39
  @upto ||= self.class.default_upto
39
40
  end
41
+
42
+ def history
43
+ @history ||= self.class.default_history(item)
44
+ end
45
+
46
+ def self.default_history item
47
+ item.respond_to?(:value_type) ? Item::VALUE_CODES[item.value_type] : 3
48
+ end
40
49
 
41
50
  def raw_data
42
51
  @raw_data ||= []
@@ -60,7 +69,8 @@ module Rubix
60
69
  super().merge({
61
70
  :itemids => [options[:item_id].to_s],
62
71
  :time_from => (options[:from] || default_from).to_i.to_s,
63
- :time_till => (options[:upto] || default_upto).to_i.to_s
72
+ :time_till => (options[:upto] || default_upto).to_i.to_s,
73
+ :history => (options[:history] || default_history(options[:item])).to_s
64
74
  })
65
75
  end
66
76
 
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <zabbix_export version="1.0" date="13.02.12" time="19.56">
3
+ <hosts>
4
+ <host name="test">
5
+ <proxy_hostid>0</proxy_hostid>
6
+ <useip>1</useip>
7
+ <dns></dns>
8
+ <ip>127.0.0.1</ip>
9
+ <port>10050</port>
10
+ <status>3</status>
11
+ <useipmi>0</useipmi>
12
+ <ipmi_ip>127.0.0.1</ipmi_ip>
13
+ <ipmi_port>623</ipmi_port>
14
+ <ipmi_authtype>0</ipmi_authtype>
15
+ <ipmi_privilege>2</ipmi_privilege>
16
+ <ipmi_username></ipmi_username>
17
+ <ipmi_password></ipmi_password>
18
+ <groups>
19
+ <group>Templates</group>
20
+ </groups>
21
+ <triggers/>
22
+ <items/>
23
+ <templates/>
24
+ <graphs/>
25
+ <macros/>
26
+ </host>
27
+ </hosts>
28
+ <dependencies/>
29
+ </zabbix_export>
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rubix::Connection do
4
+
5
+ before do
6
+ integration_test
7
+ end
8
+
9
+ after do
10
+ truncate_all_tables
11
+ end
12
+
13
+ it "can perform an authorized GET request to the homepage" do
14
+ response = Rubix.connection.web_request("GET", "/")
15
+ response.should_not be_nil
16
+ response.code.to_i.should == 200
17
+ response.body.should_not include('guest')
18
+ response.body.should include($RUBIX_INTEGRATION_TEST['username'])
19
+ end
20
+
21
+ it "can perform an authorized POST request to the homepage" do
22
+ response = Rubix.connection.web_request("POST", "/", :foo => 'bar', :baz => 'buzz')
23
+ response.should_not be_nil
24
+ response.code.to_i.should == 200
25
+ response.body.should_not include('guest')
26
+ response.body.should include($RUBIX_INTEGRATION_TEST['username'])
27
+ end
28
+
29
+ it "can perform an authorized multipart POST request to the homepage" do
30
+ response = Rubix.connection.web_request("POST", "/", :foo => File.new(data_path('test_template.xml')))
31
+ response.should_not be_nil
32
+ response.code.to_i.should == 200
33
+ response.body.should_not include('guest')
34
+ response.body.should include($RUBIX_INTEGRATION_TEST['username'])
35
+ end
36
+
37
+ end
38
+
@@ -22,6 +22,11 @@ describe "Templates" do
22
22
  template = Rubix::Template.new(:name => 'rubix_spec_template_1', :host_groups => [@host_group_1])
23
23
  template.save.should be_true
24
24
  end
25
+
26
+ it "can be imported" do
27
+ Rubix::Template.import(File.new(data_path('test_template.xml')))
28
+ Rubix::Template.find(:name => 'test').should_not be_nil
29
+ end
25
30
 
26
31
  end
27
32
 
@@ -45,8 +50,4 @@ describe "Templates" do
45
50
 
46
51
  end
47
52
 
48
- it "should be able to import and export a template" do
49
- pending "Learning how to import/export XML via the API"
50
- end
51
-
52
53
  end
@@ -5,7 +5,7 @@ describe "TimeSeries" do
5
5
  before do
6
6
  integration_test
7
7
  @host_group = ensure_save(Rubix::HostGroup.new(:name => 'rubix_spec_host_group_1'))
8
- @host = ensure_save(Rubix::Host.new(:name => 'rubix_spec_host_1', :host_groups => [@host_group]))
8
+ @host = ensure_save(Rubix::Host.new(:name => 'rubix_spec_host_1', :host_groups => [@host_group]))
9
9
  end
10
10
 
11
11
  after do
@@ -18,6 +18,10 @@ describe "TimeSeries" do
18
18
  Rubix::TimeSeries.new.from.should_not be_nil
19
19
  Rubix::TimeSeries.new.upto.should_not be_nil
20
20
  end
21
+
22
+ it "should set a default item type (history)" do
23
+ Rubix::TimeSeries.new.history.should_not be_nil
24
+ end
21
25
 
22
26
  it "should accept a given timeframe when querying" do
23
27
  Rubix::TimeSeries.find_params(:item_id => 100, :from => '1327543430', :upto => Time.at(1327543450))[:time_from].should == '1327543430'
@@ -41,13 +45,14 @@ describe "TimeSeries" do
41
45
 
42
46
  before do
43
47
  @item = ensure_save(Rubix::Item.new(:host_id => @host.id, :key => 'foo.bar.baz', :value_type => :unsigned_int, :description => "rubix item description"))
48
+ @history = create_history(@item)
44
49
  end
45
50
 
46
51
  it "should parse the results properly" do
47
52
  @ts = Rubix::TimeSeries.find(:item_id => @item.id)
48
53
  @ts.should_not be_nil
49
- @ts.should_receive(:raw_data).and_return([{'clock' => '1327543429', 'value' => '3'}, {'clock' => '1327543430'}])
50
- @ts.parsed_data.should == [{'time' => Time.at(1327543429), 'value' => 3}]
54
+ @ts.raw_data.should == @history.reverse
55
+ @ts.parsed_data.should == @history.reverse.collect{|history| { 'time' => Time.at(history["clock"].to_i), 'value' => history["value"].to_i } }
51
56
  end
52
57
 
53
58
  end
@@ -20,24 +20,50 @@ describe Rubix::Connection do
20
20
  @connection = Rubix::Connection.new('localhost/api.php', 'username', 'password')
21
21
  end
22
22
 
23
- it "should attempt to authorize itself without being asked" do
24
- @connection.should_receive(:authorize!)
25
- @connection.request('foobar', {})
26
- end
23
+ describe "sending API requests" do
27
24
 
28
- it "should not repeatedly authorize itself" do
29
- @mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
30
- @connection.request('foobar', {})
31
- @connection.should_not_receive(:authorize!)
32
- @connection.request('foobar', {})
33
- end
25
+ it "should attempt to authorize itself without being asked" do
26
+ @connection.should_receive(:authorize!)
27
+ @connection.request('foobar', {})
28
+ end
29
+
30
+ it "should not repeatedly authorize itself" do
31
+ @mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
32
+ @connection.request('foobar', {})
33
+ @connection.should_not_receive(:authorize!)
34
+ @connection.request('foobar', {})
35
+ end
36
+
37
+ it "should increment its request ID" do
38
+ @mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
39
+ @connection.request('foobar', {})
40
+ @connection.request('foobar', {})
41
+ @connection.request_id.should == 3 # it's the number used for the *next* request
42
+ end
34
43
 
35
- it "should increment its request ID" do
36
- @mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
37
- @connection.request('foobar', {})
38
- @connection.request('foobar', {})
39
- @connection.request_id.should == 3 # it's the number used for the *next* request
40
44
  end
41
45
 
42
- end
46
+ describe "sending web requests" do
47
+
48
+ it "should attempt to authorize itself without being asked" do
49
+ @connection.should_receive(:authorize!)
50
+ @connection.web_request("GET", "/")
51
+ end
52
+
53
+ it "should not repeatedly authorize itself" do
54
+ @mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
55
+ @connection.web_request("GET", "/")
56
+ @connection.should_not_receive(:authorize!)
57
+ @connection.web_request("GET", "/")
58
+ end
59
+
60
+ it "should NOT increment its request ID" do
61
+ @mock_response.stub!(:body).and_return(@good_auth_response, @blah_response, @blah_response)
62
+ @connection.web_request("GET", "/")
63
+ @connection.web_request("GET", "/")
64
+ @connection.request_id.should == 1
65
+ end
66
+
67
+ end
43
68
 
69
+ end
@@ -46,6 +46,14 @@ module Rubix
46
46
  raise e
47
47
  end
48
48
  end
49
+
50
+ def create_history item
51
+ (1..10).to_a.collect do |i|
52
+ history = { "itemid" => item.id.to_s, "clock" => (Time.now.to_i - 5*i).to_s, "value" => rand(100).to_s }
53
+ $RUBIX_MYSQL_CLIENT.query("INSERT INTO history_uint (#{history.keys.join(', ')}) VALUES (#{history.values.join(', ')})")
54
+ history
55
+ end
56
+ end
49
57
 
50
58
  def self.setup_integration_tests test_yml_path
51
59
  return unless File.exist?(test_yml_path)
@@ -68,7 +76,7 @@ module Rubix
68
76
  $RUBIX_INTEGRATION_TEST = api_connection
69
77
  end
70
78
 
71
- RUBIX_TABLES_TO_TRUNCATE = %w[applications groups hostmacro hosts hosts_groups hosts_profiles hosts_profiles_ext hosts_templates items items_applications profiles triggers trigger_depends]
79
+ RUBIX_TABLES_TO_TRUNCATE = %w[applications groups hostmacro hosts hosts_groups hosts_profiles hosts_profiles_ext hosts_templates items items_applications profiles triggers trigger_depends history sessions]
72
80
 
73
81
  def self.truncate_all_tables
74
82
  return unless $RUBIX_INTEGRATION_TEST
@@ -78,6 +86,10 @@ module Rubix
78
86
  def truncate_all_tables
79
87
  IntegrationHelper.truncate_all_tables
80
88
  end
89
+
90
+ def data_path *args
91
+ File.join(File.expand_path('../../data', __FILE__), *args)
92
+ end
81
93
 
82
94
  end
83
95
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 4
8
- - 0
9
- version: 0.4.0
8
+ - 1
9
+ version: 0.4.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Dhruv Bansal
@@ -134,6 +134,7 @@ files:
134
134
  - spec/requests/item_request_spec.rb
135
135
  - spec/requests/host_group_request_spec.rb
136
136
  - spec/requests/user_macro_request_spec.rb
137
+ - spec/requests/connection_request_spec.rb
137
138
  - spec/requests/template_request_spec.rb
138
139
  - spec/requests/trigger_request_spec.rb
139
140
  - spec/requests/host_request_spec.rb
@@ -142,6 +143,7 @@ files:
142
143
  - spec/support/integration_helper.rb
143
144
  - spec/support/response_helper.rb
144
145
  - spec/support/configliere_helper.rb
146
+ - spec/data/test_template.xml
145
147
  - LICENSE
146
148
  - README.rdoc
147
149
  - VERSION