splunker 0.0.0 → 0.0.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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A Ruby client for the [RESTful Splunk API](http://dev.splunk.com/view/rest-api-overview/SP-CAAADP8)
4
4
 
5
+ Consider this largely functional but alpha. See the TODO list below.
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -18,17 +20,75 @@ Or install it yourself as:
18
20
 
19
21
  ## Usage
20
22
 
21
- Models are on there way, but you can access resources by directly invoking the
23
+ Models are on the way, but you can access resources by directly invoking the
22
24
  HTTP helper methods.
23
25
 
26
+ # Console
27
+ To make playing around with the API client a smooth(er) experience, you can fire up our IRB wrapper by:
28
+
29
+ $ bundle exec script/console
30
+ Enabling console mode for local gem
31
+ Loading splunker gem...
32
+ Splunker:001:0> c = Splunker.client(:auth_mode => :http_auth)
33
+ #<Splunker::Client:0x007f9782b0d238 @endpoint="https://localhost:8089", @app="search", @ssl_verify=true, @request_handler=#<Splunker::Auth::HttpAuth:0x007f9782b0cf68 @client=#<Splunker::Client:0x007f9782b0d238 ...>>, @password=nil, @username=nil>
34
+
24
35
  # Basic Auth
25
36
  c = Splunker.client(:auth_mode => :http_auth, :username => "MYUSERNAME",
26
37
  :password => "MYPASSWORD", :endpoint => "https://splunk.mysite.com")
27
38
  # Returns Nokogiri::XML::Document
28
- r = c.get("/servicesNS/joshua/search/saved/searches/MySearch/history")
39
+ # Note that /servicesNS/YOUR_USERNAME/YOUR_APPNAME is prepended automatically
40
+ # to your resource.
41
+ r = c.get("/saved/searches/MySearch/history")
29
42
  # Process away
30
43
  r.xpath("...")
31
44
 
45
+ ## Exceptions
46
+
47
+ The API client raises an exception when a non-2XX [response codes](http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTusing#Response_status) is received.
48
+
49
+ <table>
50
+ <thead>
51
+ <tr>
52
+ <th>HTTP Code</th>
53
+ <th>Splunk API Error Code</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ <tr>
58
+ <td>401</td>
59
+ <td>Splunker::Errors::AuthenticationFailureError</td>
60
+ </tr>
61
+ <tr>
62
+ <td>402</td>
63
+ <td>Splunker::Errors::FeatureDisabledError</td>
64
+ </tr>
65
+ <tr>
66
+ <td>403</td>
67
+ <td>Splunker::Errors::PermissionDeniedError</td>
68
+ </tr>
69
+ <tr>
70
+ <td>404</td>
71
+ <td>Splunker::Errors::ObjectDoesNotExistError</td>
72
+ </tr>
73
+ <tr>
74
+ <td>405</td>
75
+ <td>Splunker::Errors::MethodNotAllowedError</td>
76
+ </tr>
77
+ <tr>
78
+ <td>409</td>
79
+ <td>Splunker::Errors::InvalidOperationError</td>
80
+ </tr>
81
+ <tr>
82
+ <td>500</td>
83
+ <td>Splunker::Errors::InternalServerError </td>
84
+ </tr>
85
+ <tr>
86
+ <td>Any other non-2xx response</td>
87
+ <td>Splunker::Errors::ClientError</td>
88
+ </tr>
89
+ </tbody>
90
+ </table>
91
+
32
92
  ## Contributing
33
93
 
34
94
  1. Fork it
@@ -36,3 +96,8 @@ HTTP helper methods.
36
96
  3. Commit your changes (`git commit -am 'Added some feature'`)
37
97
  4. Push to the branch (`git push origin my-new-feature`)
38
98
  5. Create new Pull Request
99
+
100
+ # TODO
101
+ * Token Auth
102
+ * Resource creation handling, blocking & polling options, with a timeout.
103
+ * Build console into gem (bin/)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.0.1
@@ -3,7 +3,21 @@ require "splunker/version"
3
3
  require "splunker/client"
4
4
  require "splunker/errors"
5
5
 
6
+ # The parent Splunker module can directly invoke any method invokable by the
7
+ # client returned in Splunker.client, so long as Splunker.client has been
8
+ # called once with options to instantiate a new client.
9
+ #
10
+ # If using the helper models, you must first configure Splunker with
11
+ # Splunker.client({ :options => "..." }), as they all reference Splunker.client
12
+ # when they make requests.
6
13
  module Splunker
14
+
15
+ # Returns a reference to the current Splunk API client if no options are
16
+ # supplied. Otherwise, generates a new Splunker client.
17
+ #
18
+ # Parameters:
19
+ # * options => See Splunker::Client's initialize method for details.
20
+ # Returns an instance of Splunker::Client
7
21
  def self.client(options={})
8
22
  unless options.empty?
9
23
  Thread.current[:splunker_client] = Splunker::Client.new(options)
@@ -12,6 +26,7 @@ module Splunker
12
26
  Thread.current[:splunker_client]
13
27
  end
14
28
 
29
+ # A reference to the Splunker logger
15
30
  def self.logger
16
31
  if @logger.nil?
17
32
  @logger = Logger.new(STDOUT)
@@ -20,6 +35,8 @@ module Splunker
20
35
  @logger
21
36
  end
22
37
 
38
+ # Parameters:
39
+ # * custom_logger => A custom logger to use instead of Logger
23
40
  def self.logger=(custom_logger)
24
41
  @logger = custom_logger
25
42
  end
@@ -7,9 +7,16 @@ end
7
7
 
8
8
  module Splunker
9
9
  module Auth
10
- def self.create(auth_type, configuration)
10
+ # Factory to create a authenticated Request wrapper for a specified auth type.
11
+ # Parameters:
12
+ # * auth_type => A symbol representing the auth type to generate. Should be a symbolized
13
+ # class name for an existing implementation. (e.g. :http_auth, :token_auth)
14
+ # * client => A reference to the Splunker client instance.
15
+ # Returns an instance of the authenticated request wrapper noted by auth_type
16
+ # Raises ArgumentError if auth_type is invalid
17
+ def self.create(auth_type, client)
11
18
  if (obj = Splunker::Auth.const_get("#{auth_type}".split('_').collect(&:capitalize).join))
12
- obj.new(configuration)
19
+ obj.new(client)
13
20
  else
14
21
  raise ArgumentError, "Unknown auth type of #{auth_type}"
15
22
  end
@@ -1,5 +1,6 @@
1
1
  module Splunker
2
2
  module Auth
3
+ # Wraps Splunk API requests with basic authentication
3
4
  class HttpAuth < SplunkAuth
4
5
  def authenticate_connection(conn)
5
6
  return if self.configured?
@@ -2,9 +2,21 @@ require 'splunker/configuration'
2
2
  require 'splunker/auth'
3
3
 
4
4
  module Splunker
5
+ # The Splunk API client.
6
+ # Methods include get, post, put, and delete HTTP helpers:
7
+ # c.get("...")
8
+ # See Splunker::Request for more details.
5
9
  class Client
6
10
  include Configuration
7
11
 
12
+ # Creates a new Splunker client instance.
13
+ # Options are:
14
+ # * :username => Required. The username to make requests on behalf of
15
+ # * :password => Required. The password to authenticate with
16
+ # * :auth_mode => Required. The authentication method to use. :http_auth or :token_auth.
17
+ # * :endpoint => ("https://localhost:8089") The host of the Splunk API
18
+ # * :ssl_verify => (true) If false, the SSL cert fro the Splunk server will not be verified.
19
+ # * :app => ("search")
8
20
  def initialize(options={})
9
21
  self.reset
10
22
 
@@ -7,6 +7,8 @@ module Splunker
7
7
  @connection = nil
8
8
  end
9
9
 
10
+ # Returns an existing, or new, Faraday instance.
11
+ # If a new connection is desired, #reset must be called.
10
12
  def connection
11
13
  if self.configuration[:endpoint].nil?
12
14
  raise ConfigurationError, "No endpoint set!"
@@ -23,6 +23,14 @@ module Splunker
23
23
  500 => InternalServerError
24
24
  }
25
25
 
26
+ # Parameters:
27
+ # * http_status => The integer representing the HTTP status
28
+ # * body => The response body. Will be passed to the
29
+ # error instance when raised, for additional
30
+ # information.
31
+ # Returns nil if no exception will be raised.
32
+ # Raises an error mapped to http_status in STATUS_CODE_TO_ERROR_MAP,
33
+ # or ClientError if status is not 2xx but no mapped error exists.
26
34
  def self.raise_error_for_status!(http_status, body)
27
35
  return nil if (200..299).include?(http_status)
28
36
 
@@ -6,7 +6,7 @@ module Splunker
6
6
  end
7
7
 
8
8
  def on_complete(env)
9
- env[:body] = Nokogiri::XML(env[:body])
9
+ env[:body] = Nokogiri::Slop(env[:body])
10
10
  Splunker.logger.debug("Response Body: #{env[:body]}")
11
11
  Splunker::Errors.raise_error_for_status!(env[:status], env[:body])
12
12
  Splunker.logger.debug "Request successful!"
@@ -16,11 +16,6 @@ module Splunker
16
16
 
17
17
  protected
18
18
 
19
- # Basic path validation. Does nothing useful atm.
20
- def self.assemble_path(path_str)
21
- path_str.gsub(/\/+/,"/")
22
- end
23
-
24
19
  # Escapes a object ID for use in a URI
25
20
  def self.escape_object_id(id_str)
26
21
  CGI.escape(id_str)
@@ -1,34 +1,81 @@
1
1
  require 'splunker/connection'
2
2
 
3
3
  module Splunker
4
+ # The Request module, to be used by classes that will make requests to
5
+ # the Splunk API.
6
+ #
7
+ # Note that any class that mixes in the Request module will also mix in
8
+ # Splunker::Connection automatically (see Splunker::Request.included)
4
9
  module Request
10
+ require 'addressable/uri'
5
11
 
12
+ # Authenticates the user (not the request) identified by
13
+ # :username and :password in the configuration.
6
14
  def authenticate; end
7
15
 
16
+ # true if #authenticate does _not_ need to be called.
8
17
  def authenticated?
9
18
  true
10
19
  end
11
20
 
21
+ # Attaches credentials to the supplied Faraday connection object.
12
22
  def authenticate_connection(conn)
13
23
  conn
14
24
  end
15
25
 
26
+ # HTTP GET helper method.
27
+ # Parameters:
28
+ # * resource => The resource under configuration[:endpoint] to make the
29
+ # request
30
+ # * parameters => GET parameters to supply with the request.
31
+ # Returns Nokogiri:XML:Document object, parsed from the response body.
32
+ # Raises an exception from Splunker::Errors in accordance with the status code.
16
33
  def get(resource, parameters={})
17
34
  request(:get, resource, parameters)
18
35
  end
19
36
 
20
- #def post(resource, body={})
21
- # request(:post, resource, nil, body)
22
- #end
37
+ def post(resource, body={})
38
+ request(:post, resource, nil, body)
39
+ end
23
40
 
24
41
  ###
25
42
  # put/delete can come as needed.
26
43
  ###
27
44
 
45
+ # HTTP request helper method.
46
+ # Parameters:
47
+ # * method => A symbol representing the HTTP method (:get, :post, :put, :delete)
48
+ # * resource => The resource under configuration[:endpoint] to make the
49
+ # request
50
+ # * parameters => GET parameters to supply with the request.
51
+ # * body => POST/PUT data
52
+ # Returns Nokogiri:XML:Document object, parsed from the response body.
53
+ # Raises an exception from Splunker::Errors in accordance with the status code.
28
54
  def request(method, resource, parameters={}, body={})
29
55
  authenticate unless authenticated?
30
56
  authenticate_connection(self.connection)
31
- self.connection.send(method, resource).body
57
+ final_resource = resource_builder(resource, parameters)
58
+ self.connection.send(method, final_resource, body).body
59
+ end
60
+
61
+ # Returns a string representing the final resource path
62
+ def resource_builder(resource, parameters={})
63
+ u = Addressable::URI.new
64
+ u.path = assemble_path("/servicesNS/#{configuration[:username]}/#{configuration[:app]}/#{resource}")
65
+ unless parameters.nil? || parameters.empty?
66
+ # Let's remap and cast everything to a string.
67
+ # Addressable doesn't handle values like true well.
68
+ final_parameters = {}
69
+ parameters.map do |key, value|
70
+ final_parameters[key] = "#{value}"
71
+ end
72
+ u.query_values = final_parameters
73
+ end
74
+ u.to_s
75
+ end
76
+
77
+ def assemble_path(path_str)
78
+ path_str.gsub(/\/+/,"/")
32
79
  end
33
80
 
34
81
  def self.included(base)
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ describe Splunker::Request do
3
+ context "successful requests" do
4
+ it "should return the response body as an Nokogiri::XML::Document" do
5
+ stub_fixture(client, "generic/search/results/get", "search_results_from_job.xml",
6
+ :parameters => {:parameter1 => "OK"})
7
+ client.get("generic/search/results/get").should be_a(Nokogiri::XML::Document)
8
+ end
9
+
10
+ it "should successfully perform post requests" do
11
+ stub_fixture(client, "generic/search/results/post", "search_results_from_job.xml",
12
+ :method => :post, :data => {:parameter1 => "OK"})
13
+ client.post("generic/search/results/post", :parameter1 => "OK").should be_a(Nokogiri::XML::Document)
14
+ end
15
+ end
16
+
17
+ context "authentication" do # TODO: Once we have token_auth in place.
18
+ it "should not authenticate unless needed" do
19
+ end
20
+ it "should authenticated if needed" do
21
+ end
22
+ it "should retry authentication once if a 401 is returned" do
23
+ end
24
+ end
25
+
26
+ context "http errors" do
27
+ it "should raise a mapped exception with a known HTTP error" do
28
+ stub_http_error(client, 405)
29
+ expect {
30
+ client.get("error/405")
31
+ }.to raise_error Splunker::Errors::MethodNotAllowedError
32
+ end
33
+ it "should raise a client error with an unknown status code" do
34
+ stub_http_error(client, 499)
35
+ expect {
36
+ client.get("error/499")
37
+ }.to raise_error Splunker::Errors::ClientError
38
+ end
39
+ end
40
+ end
@@ -1,4 +1,5 @@
1
1
  require 'rspec'
2
+ require 'webmock/rspec'
2
3
  require 'splunker'
3
4
  require 'rspec_let_definitions'
4
5
 
@@ -6,4 +7,34 @@ RSpec.configure do |config|
6
7
  config.color_enabled = true
7
8
  config.formatter = 'documentation'
8
9
  config.include Splunker::RspecLetDefinitions
10
+ config.include WebMock::API
11
+ end
12
+
13
+ def fixture(file)
14
+ File.new( File.expand_path('../fixtures', __FILE__) + "/" + file)
15
+ end
16
+
17
+ def basic_auth_resource_builder(client, resource)
18
+ path = client.resource_builder(resource)
19
+ endpoint = client.configuration[:endpoint]
20
+ endpoint.gsub!("https://", "https://#{client.configuration[:username]}:#{client.configuration[:password]}@")
21
+
22
+ "#{endpoint}#{path}"
23
+ end
24
+
25
+ def stub_basic_auth_request(client, resource, method=:get)
26
+ uri = basic_auth_resource_builder(client,resource)
27
+ stub_request(method, uri)
28
+ end
29
+
30
+ def stub_http_error(client, code, method=:get)
31
+ stub_basic_auth_request(client, "error/#{code}", method).to_return(:status => code)
32
+ end
33
+
34
+ def stub_fixture(client, resource, fixture_file, options={:method => :get, :status=>200, :parameters => {},
35
+ :post_data => {}})
36
+ code = (options[:status] || 200)
37
+ method = (options[:method] || :get)
38
+ stub_basic_auth_request(client, resource, method).to_return(:status => code,
39
+ :body => fixture(fixture_file))
9
40
  end
@@ -0,0 +1,17 @@
1
+ describe Splunker::Request do
2
+ context "assembling a path" do
3
+ it "should strip repeated slashes" do
4
+ client.assemble_path("//search//joshua/saved/searches").should eq("/search/joshua/saved/searches")
5
+ end
6
+ end
7
+
8
+ context "building a resource path" do
9
+ it "should assemble the path from the username and app name" do
10
+ client.resource_builder("saved/searches").should eq("/servicesNS/tido/search/saved/searches")
11
+ end
12
+ it "should assemble the path with query parameters" do
13
+ client.resource_builder("saved/searches", :parameter1 => "me",
14
+ :parameter2 => true).should eq("/servicesNS/tido/search/saved/searches?parameter1=me&parameter2=true")
15
+ end
16
+ end
17
+ end
@@ -17,6 +17,7 @@ Gem::Specification.new do |gem|
17
17
 
18
18
  gem.add_dependency "nokogiri"
19
19
  gem.add_dependency "faraday"
20
+ gem.add_dependency "addressable"
20
21
  gem.add_development_dependency "rspec", "~> 2.11"
21
22
  gem.add_development_dependency "rake", "~> 0.9.2"
22
23
  gem.add_development_dependency "webmock"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: splunker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-10 00:00:00.000000000 Z
12
+ date: 2012-09-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: addressable
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
46
62
  - !ruby/object:Gem::Dependency
47
63
  name: rspec
48
64
  requirement: !ruby/object:Gem::Requirement
@@ -125,6 +141,7 @@ files:
125
141
  - lib/splunker/version.rb
126
142
  - script/console
127
143
  - spec/fixtures/search_results_from_job.xml
144
+ - spec/functional/request_spec.rb
128
145
  - spec/rspec_let_definitions.rb
129
146
  - spec/spec_helper.rb
130
147
  - spec/unit/auth/http_auth_spec.rb
@@ -132,6 +149,7 @@ files:
132
149
  - spec/unit/client_spec.rb
133
150
  - spec/unit/configuration_spec.rb
134
151
  - spec/unit/errors_spec.rb
152
+ - spec/unit/request_spec.rb
135
153
  - spec/unit/splunker_spec.rb
136
154
  - splunker.gemspec
137
155
  homepage: ''
@@ -160,6 +178,7 @@ specification_version: 3
160
178
  summary: A Ruby client for the Splunk API
161
179
  test_files:
162
180
  - spec/fixtures/search_results_from_job.xml
181
+ - spec/functional/request_spec.rb
163
182
  - spec/rspec_let_definitions.rb
164
183
  - spec/spec_helper.rb
165
184
  - spec/unit/auth/http_auth_spec.rb
@@ -167,4 +186,5 @@ test_files:
167
186
  - spec/unit/client_spec.rb
168
187
  - spec/unit/configuration_spec.rb
169
188
  - spec/unit/errors_spec.rb
189
+ - spec/unit/request_spec.rb
170
190
  - spec/unit/splunker_spec.rb