rubix 0.4.0 → 0.4.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.
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