splunk-sdk-ruby 0.1.0 → 0.8.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.
@@ -0,0 +1,136 @@
1
+ #--
2
+ # Copyright 2011-2013 Splunk, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"): you may
5
+ # not use this file except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations
14
+ # under the License.
15
+ #++
16
+
17
+ require_relative '../collection'
18
+
19
+ ##
20
+ # Provide a class representing a collection of input kinds.
21
+ #
22
+ module Splunk
23
+ ##
24
+ # A collection of input types.
25
+ #
26
+ # Inputs in the Splunk REST API are arranged in what looks like a
27
+ # directory structure, as in
28
+ #
29
+ # monitor/
30
+ # tcp/
31
+ # cooked/
32
+ # raw/
33
+ # udp/
34
+ #
35
+ # You get the top level directory by calling +inputs+ on your +Service+.
36
+ # Then you can use it as if it were a Hash. If you fetch an entry that has
37
+ # subtypes, such as +tcp+, you get another +InputKinds+ containing the types
38
+ # in that entry. If you fetch an entry that doesn't have subtypes, such as
39
+ # "udp", then you get an +Inputs+ object (a subclass of +Collection+)
40
+ # containing specific inputs.
41
+ #
42
+ # *Example*:
43
+ #
44
+ # # Returns an InputKinds collection
45
+ # tcp_inputs = service.inputs["tcp"]
46
+ # tcp_inputs.has_key?("raw") # ==> true
47
+ # tcp_inputs.has_key?("cooked") # ==> true
48
+ #
49
+ # # A read only collection of all the inputs in Splunk.
50
+ # service.inputs["all"]
51
+ #
52
+ # # An Inputs object containing all the UDP inputs in Splunk.
53
+ # service.inputs["udp"]
54
+ #
55
+ class InputKinds < ReadOnlyCollection
56
+ def fetch(name, namespace=nil)
57
+ request_args = {:resource => @resource + [name]}
58
+ if not namespace.nil?
59
+ request_args[:namespace] = namespace
60
+ end
61
+
62
+ begin
63
+ response = @service.request(request_args)
64
+ rescue SplunkHTTPError => err
65
+ if err.code == 404
66
+ return nil
67
+ else
68
+ raise err
69
+ end
70
+ end
71
+
72
+ feed = AtomFeed.new(response.body)
73
+ if feed.metadata["links"].has_key?("create")
74
+ Inputs.new(@service, resource + [name])
75
+ elsif name == "all"
76
+ ReadOnlyCollection.new(@service, resource + [name])
77
+ else
78
+ InputKinds.new(@service, resource + [name])
79
+ end
80
+ end
81
+ end
82
+
83
+ ##
84
+ # A collection of specific inputs.
85
+ #
86
+ class Inputs < Collection
87
+ def initialize(service, resource)
88
+ super(service, resource)
89
+ @always_fetch = true
90
+ end
91
+
92
+ def create(name, args={})
93
+ body_args = args.clone()
94
+ body_args["name"] = name
95
+
96
+ request_args = {
97
+ :method => :POST,
98
+ :resource => @resource,
99
+ :body => body_args
100
+ }
101
+ if args.has_key?(:namespace)
102
+ request_args[:namespace] = body_args.delete(:namespace)
103
+ end
104
+
105
+ @service.request(request_args)
106
+
107
+ # If we have created a oneshot input, no actual entity
108
+ # is created. We return nil here in that case.
109
+ if @resource == ["data", "inputs", "oneshot"]
110
+ return nil
111
+ end
112
+
113
+ # TCP and UDP inputs have a key restrictToHost. If it is set
114
+ # then they are created with hostname:port as their resource
115
+ # instead of just port, and we must adjust our behavior
116
+ # accordingly.
117
+ if args.has_key?(:restrictToHost)
118
+ name = args[:restrictToHost] + ":" + name
119
+ end
120
+
121
+ fetch_args = {:method => :GET,
122
+ :resource => @resource + [name]}
123
+ if args.has_key?(:namespace)
124
+ fetch_args[:namespace] = args[:namespace]
125
+ end
126
+ response = @service.request(fetch_args)
127
+
128
+ feed = AtomFeed.new(response.body)
129
+ raise StandardError.new("No entities returned") if feed.entries.empty?
130
+ entity = atom_entry_to_entity(feed.entries[0])
131
+ raise StandardError.new("Found nil entity.") if entity.nil?
132
+ return entity
133
+ end
134
+
135
+ end
136
+ end
@@ -51,7 +51,7 @@ module Splunk
51
51
  # Creates an asynchronous search job.
52
52
  #
53
53
  # The search job requires a _query_, and takes a hash of other, optional
54
- # arguments, which are documented in the {Splunk REST documentation}[http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTsearch#search.2Fjobs - POST].
54
+ # arguments, which are documented in the {Splunk REST documentation}[http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTsearch#search.2Fjobs].
55
55
  #
56
56
  def create(query, args={})
57
57
  if args.has_key?(:exec_mode)
@@ -81,6 +81,10 @@ module Splunk
81
81
  def create_oneshot(query, args={})
82
82
  args[:search] = query
83
83
  args[:exec_mode] = 'oneshot'
84
+ # Suppress segmentation (<sg> tags in the XML response) by default:
85
+ if !args.has_key?(:segmentation)
86
+ args[:segmentation] = "none"
87
+ end
84
88
  response = @service.request(:method => :POST,
85
89
  :resource => @resource,
86
90
  :body => args)
@@ -100,6 +104,10 @@ module Splunk
100
104
  #
101
105
  def create_export(query, args={})
102
106
  args["search"] = query
107
+ # Suppress segmentation (<sg> tags in the XML response) by default:
108
+ if !args.has_key?(:segmentation)
109
+ args[:segmentation] = "none"
110
+ end
103
111
  response = @service.request(:method => :GET,
104
112
  :resource => @resource + ["export"],
105
113
  :query => args)
@@ -232,13 +232,13 @@ module Splunk
232
232
  # * +method+: The HTTP method to use (one of +:GET+, +:POST+, or +:DELETE+;
233
233
  # default: +:GET+).
234
234
  # * +namespace+: The namespace to request a resource from Splunk in. Must
235
- # be a +Namespace+ object. (default: the value of +@namespace+ on
235
+ # be a +Namespace+ object. (default: the value of @+namespace+ on
236
236
  # this +Context+)
237
237
  # * +resource+: An array of strings specifying the components of the path
238
238
  # to the resource after the namespace. The strings should not be URL
239
239
  # encoded, as that will be handled by +request+. (default: [])
240
240
  # * +query+: A hash containing the values to be encoded as
241
- # the query (the part following +?+) in the URL. Nothing should be URL
241
+ # the query (the part following "?") in the URL. Nothing should be URL
242
242
  # encoded as +request+ will do the encoding. If you need to pass multiple
243
243
  # values for the same key, insert them as an Array as the value of their
244
244
  # key into the Hash, and they will be properly encoded as a sequence of
@@ -332,7 +332,7 @@ module Splunk
332
332
  # make a request to.
333
333
  # * +:method+: (+:GET+, +:POST+, or +:DELETE+) The HTTP method to use.
334
334
  # * +query+: A hash containing the values to be encoded as
335
- # the query (the part following +?+) in the URL. Nothing should be URL
335
+ # the query (the part following "?") in the URL. Nothing should be URL
336
336
  # encoded as +request+ will do the encoding. If you need to pass multiple
337
337
  # values for the same key, insert them as an +Array+ as the value of their
338
338
  # key into the Hash, and they will be properly encoded as a sequence of
@@ -445,7 +445,8 @@ module Splunk
445
445
  " of the Splunk Ruby SDK"})
446
446
 
447
447
  # Make the actual restart request.
448
- request(:resource => ["server", "control", "restart"])
448
+ request(:method => :POST,
449
+ :resource => ["server", "control", "restart"])
449
450
 
450
451
  # Clear our old token, which will no longer work after the restart.
451
452
  logout()
@@ -478,6 +479,8 @@ module Splunk
478
479
  request(:resource => ["data", "indexes"])
479
480
  rescue Errno::ECONNREFUSED, EOFError, Errno::ECONNRESET
480
481
  return false
482
+ rescue OpenSSL::SSL::SSLError
483
+ return false
481
484
  rescue SplunkHTTPError
482
485
  # Splunk is up, because it responded with a proper HTTP error
483
486
  # that our SplunkHTTPError parser understood.
@@ -21,24 +21,12 @@ require_relative 'synonyms'
21
21
 
22
22
  module Splunk
23
23
  ##
24
- # Class representing individual entities in Splunk.
25
- #
26
- # +Entity+ objects represent individual items such as indexes, users, roles,
27
- # etc. They are usually contained within +Collection+ objects.
24
+ # ReadOnlyEntity represents entities that can be read, but not created or
25
+ # updated, via the REST API. The canonical example is a modular input kind.
28
26
  #
29
- # The basic, identifying information for an +Entity+ (name, namespace, path
30
- # of the collection containing entity, and the service it's on) is all
31
- # accessible via getters (+name+, +namespace+, +resource+, +service+). All
32
- # the fields containing the +Entity+'s state, such as the capabilities of
33
- # a role or whether an app should check for updates, are accessible with
34
- # the [] operator (for instance, +role+["+capabilities+"] or
35
- # +app+["+check_for_updates+"]).
36
- #
37
- # +Entity+ objects cache their state, so each lookup of a field does not
38
- # make a roundtrip to the server. The state may be refreshed by calling
39
- # the +refresh+ method on the +Entity+.
40
- #
41
- class Entity
27
+ class ReadOnlyEntity
28
+ # ReadOnlyEntity was factored out of Entity to avoid having to add
29
+ # special behavior to modular input kinds.
42
30
  extend Synonyms
43
31
 
44
32
  def initialize(service, namespace, resource, name, state=nil) # :nodoc:
@@ -86,17 +74,6 @@ module Splunk
86
74
  #
87
75
  attr_reader :service
88
76
 
89
- ##
90
- # Deletes this entity from the server.
91
- #
92
- # Returns: +nil+.
93
- #
94
- def delete()
95
- @service.request({:method => :DELETE,
96
- :namespace => @namespace,
97
- :resource => @resource + [name]})
98
- end
99
-
100
77
  ##
101
78
  # Fetches the field _key_ on this entity.
102
79
  #
@@ -138,7 +115,7 @@ module Splunk
138
115
  # +Entity+. If you specify one or more +Strings+ or +Arrays+ of +Strings+,
139
116
  # all the keys specified in the arguments will be returned in the +Hash+.
140
117
  #
141
- # Returns: a +Hash+ with +Strings+ as keys, and +Strings+ or +Hashes+ or
118
+ # Returns: a +Hash+ with +Strings+ as keys, and +Strings+ or +Hashes+ or
142
119
  # +Arrays+ as values.
143
120
  #
144
121
  def read(*field_list)
@@ -187,6 +164,37 @@ module Splunk
187
164
  @state = feed.entries[0]
188
165
  self
189
166
  end
167
+ end
168
+
169
+ ##
170
+ # Class representing individual entities in Splunk.
171
+ #
172
+ # +Entity+ objects represent individual items such as indexes, users, roles,
173
+ # etc. They are usually contained within +Collection+ objects.
174
+ #
175
+ # The basic, identifying information for an +Entity+ (name, namespace, path
176
+ # of the collection containing entity, and the service it's on) is all
177
+ # accessible via getters (+name+, +namespace+, +resource+, +service+). All
178
+ # the fields containing the +Entity+'s state, such as the capabilities of
179
+ # a role or whether an app should check for updates, are accessible with
180
+ # the [] operator (for instance, +role+["+capabilities+"] or
181
+ # +app+["+check_for_updates+"]).
182
+ #
183
+ # +Entity+ objects cache their state, so each lookup of a field does not
184
+ # make a roundtrip to the server. The state may be refreshed by calling
185
+ # the +refresh+ method on the +Entity+.
186
+ #
187
+ class Entity < ReadOnlyEntity
188
+ ##
189
+ # Deletes this entity from the server.
190
+ #
191
+ # Returns: +nil+.
192
+ #
193
+ def delete()
194
+ @service.request({:method => :DELETE,
195
+ :namespace => @namespace,
196
+ :resource => @resource + [name]})
197
+ end
190
198
 
191
199
  ##
192
200
  # Updates the values on the Entity specified in the arguments.
@@ -105,10 +105,14 @@ module Splunk
105
105
  # Returns: a stream that can be read with +ResultsReader+.
106
106
  #
107
107
  def events(args={})
108
+ # Suppress segmentation (<sg> tags in the XML response) by default:
109
+ if !args.has_key?(:segmentation)
110
+ args[:segmentation] = "none"
111
+ end
108
112
  response = @service.request(
109
113
  :method => :GET,
110
114
  :resource => @resource + [sid, "events"],
111
- :body => args)
115
+ :query => args)
112
116
  return response.body
113
117
  end
114
118
 
@@ -192,10 +196,14 @@ module Splunk
192
196
  # Returns: a stream readable by +ResultsReader+.
193
197
  #
194
198
  def preview(args={})
199
+ # Suppress segmentation (<sg> tags in the XML response) by default:
200
+ if !args.has_key?(:segmentation)
201
+ args[:segmentation] = "none"
202
+ end
195
203
  response = @service.request(:method => :GET,
196
204
  :resource => @resource +
197
205
  [sid, "results_preview"],
198
- :body => args)
206
+ :query => args)
199
207
  return response.body
200
208
  end
201
209
 
@@ -247,7 +255,7 @@ module Splunk
247
255
  end
248
256
 
249
257
  ##
250
- # Return the +Job's+ search id.
258
+ # Return the +Job+'s search id.
251
259
  #
252
260
  # Returns: a +String+.
253
261
  #
@@ -268,7 +276,7 @@ module Splunk
268
276
  # duration), and +:time+ (a string representing the bucket's time in human
269
277
  # readable form).
270
278
  #
271
- # Returns: an +Array+ of +Hash+es describing each time bucket.
279
+ # Returns: an +Array+ of Hashes describing each time bucket.
272
280
  #
273
281
  def timeline(args={})
274
282
  response = @service.request(:resource => @resource + [sid, "timeline"],
@@ -0,0 +1,47 @@
1
+ #--
2
+ # Copyright 2011-2013 Splunk, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"): you may
5
+ # not use this file except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations
14
+ # under the License.
15
+ #++
16
+
17
+ require_relative '../entity'
18
+
19
+ module Splunk
20
+ ##
21
+ # Class representing a family of user defined inputs on the server.
22
+ #
23
+ # Modular input kinds define new, first-class input kinds on the server.
24
+ # This endpoint provides access to a list of the arguments and parameters
25
+ # of the various kinds defined. The actual inputs of these kinds can be
26
+ # accessed via the Serve#inputs method.
27
+ #
28
+ # Modular input kinds are read only, so there is no update or delete method
29
+ # on this class.
30
+ #
31
+ class ModularInputKind < ReadOnlyEntity
32
+ ##
33
+ # Return a Hash of all the arguments support by this modular input kind.
34
+ #
35
+ # The keys in the Hash are the names of the arguments. The values are
36
+ # additional Hashes giving the metadata about each argument. The possible
37
+ # keys in those Hashes are +"title"+, +"description"+,
38
+ # +"required_on_create``+, +"required_on_edit"+, +"data_type"+. Each value is
39
+ # a string. It should be one of +"true"+ or +"false"+ for
40
+ # +"required_on_create"+ and +"required_on_edit"+, and one of +"boolean"+,
41
+ # +"string"+, or +"number"+ for +"data_type"+.
42
+ #
43
+ def arguments
44
+ @state["content"]["endpoint"]["args"]
45
+ end
46
+ end
47
+ end
@@ -98,6 +98,18 @@ module Splunk
98
98
  # in this set, in the order they should be displayed (if you're going
99
99
  # to make a table or the like)
100
100
  #
101
+ # The values yielded by calling +each+ and similar methods on +ResultsReader+
102
+ # are of class +Event+, which is a subclass of +Hash+ with one extra method,
103
+ # +segmented_raw+. The fields of the event are available as values in the hash,
104
+ # with no escaped characters and no XML tags. The +_raw+ field, however, is
105
+ # returned with extra XML specifying how terms should be highlighted for
106
+ # display, and this full XML form is available by called the +segmented_raw+
107
+ # method. The XML returned looks something like:
108
+ #
109
+ # "<v xml:space=\"preserve\" trunc=\"0\">127.0.0.1 - admin
110
+ # [11/Feb/2013:10:42:49.790 -0800] \"POST /services/search/jobs/export
111
+ # HTTP/1.1\" 200 440404 - - - 257ms</v>"
112
+ #
101
113
  # *Example*:
102
114
  #
103
115
  # require 'splunk-sdk-ruby'
@@ -105,11 +117,12 @@ module Splunk
105
117
  # service = Splunk::connect(:username => "admin", :password => "changeme")
106
118
  #
107
119
  # stream = service.jobs.create_oneshot("search index=_internal | head 10")
108
- # reader = ResultsReader.new(stream)
120
+ # reader = Splunk::ResultsReader.new(stream)
109
121
  # puts reader.is_preview?
110
122
  # # Prints: false
111
123
  # reader.each do |result|
112
- # puts result
124
+ # puts result # Prints the fields in the result as a Hash
125
+ # puts result.segmented_raw() # Prints the XML version of the _raw field
113
126
  # end
114
127
  # # Prints a sequence of Hashes containing events.
115
128
  #
@@ -137,7 +150,7 @@ module Splunk
137
150
  def initialize(text_or_stream)
138
151
  if text_or_stream.nil?
139
152
  stream = StringIO.new("")
140
- elsif stream.is_a?(ExportStream)
153
+ elsif text_or_stream.is_a?(ExportStream)
141
154
  # The sensible behavior on streams from the export endpoints is to
142
155
  # skip all preview results and concatenate all others. The export
143
156
  # functions wrap their streams in ExportStream to mark that they need
@@ -154,11 +167,9 @@ module Splunk
154
167
  stream = text_or_stream
155
168
  end
156
169
 
157
- if stream.eof?
170
+ if !stream.nil? and stream.eof?
158
171
  @is_preview = nil
159
172
  @fields = []
160
- elsif stream.is_a?(ExportStream)
161
-
162
173
  else
163
174
  # We use a SAX parser. +listener+ is the event handler, but a SAX
164
175
  # parser won't usually transfer control during parsing.
@@ -186,8 +197,7 @@ module Splunk
186
197
  if @is_export
187
198
  warn "[DEPRECATED] Do not use ResultsReader on the output of the " +
188
199
  "export endpoint. Use MultiResultsReader instead."
189
- reader = MultiResultsReader.new(@stream).final_results()
190
- enum = reader.each()
200
+ enum = @reader.each()
191
201
  else
192
202
  enum = Enumerator.new() do |yielder|
193
203
  if !@iteration_fiber.nil? # Handle the case of empty files
@@ -219,6 +229,24 @@ module Splunk
219
229
  end
220
230
  end
221
231
 
232
+ ##
233
+ # +Event+ represents a single event returned from a +ResultsReader+.
234
+ #
235
+ # +Event+ is a subclass of +Hash+, adding a single method +segmented_raw()+
236
+ # which returns a string containing the XML of the raw event, as opposed
237
+ # to the unescaped, raw strings returned by fetching a particular field
238
+ # via [].
239
+ #
240
+ class Event < Hash
241
+ @raw_xml = nil
242
+
243
+ attr_writer :raw_xml
244
+
245
+ def segmented_raw
246
+ @raw_xml
247
+ end
248
+ end
249
+
222
250
  ##
223
251
  # +ResultsListener+ is the SAX event handler for +ResultsReader+.
224
252
  #
@@ -269,7 +297,7 @@ module Splunk
269
297
  elsif name == "result"
270
298
  @state = :result
271
299
  @current_offset = Integer(attributes["offset"])
272
- @current_result = {}
300
+ @current_result = Event.new()
273
301
  end
274
302
  end,
275
303
  :end_element => lambda do |name|
@@ -326,7 +354,23 @@ module Splunk
326
354
  @current_value = nil
327
355
  elsif name == "text" || name == "v"
328
356
  @state = :field_values
329
- @current_scratch = ""
357
+ @current_text = ""
358
+ s = ["v"] + attributes.map do |entry|
359
+ key, value = entry
360
+ # Nokogiri and REXML both drop the namespaces of attributes,
361
+ # and there is no way to recover them. To reconstruct the
362
+ # XML (since we can't get at its raw form) we put in the
363
+ # one instance of a namespace on an attribute that shows up
364
+ # in what Splunk returns. Yes, this is a terribly, ugly
365
+ # kludge.
366
+ if key == "space"
367
+ prefixed_key = "xml:space"
368
+ else
369
+ prefixed_key = key
370
+ end
371
+ "#{prefixed_key}=\"#{value}\""
372
+ end
373
+ @current_xml = "<" + s.join(" ") + ">"
330
374
  end
331
375
  end,
332
376
  :end_element => lambda do |name|
@@ -346,6 +390,11 @@ module Splunk
346
390
  else
347
391
  @current_result[@current_field] = @current_value
348
392
  end
393
+
394
+ if @current_field == "_raw"
395
+ @current_result.raw_xml = @current_xml
396
+ end
397
+
349
398
  @current_field = nil
350
399
  @current_value = nil
351
400
  end
@@ -356,19 +405,23 @@ module Splunk
356
405
  :end_element => lambda do |name|
357
406
  if name == "text" || name == "v"
358
407
  if @current_value == nil
359
- @current_value = @current_scratch
408
+ @current_value = @current_text
360
409
  elsif @current_value.is_a?(Array)
361
- @current_value << @current_scratch
410
+ @current_value << @current_text
362
411
  else
363
- @current_value = [@current_value, @current_scratch]
412
+ @current_value = [@current_value, @current_text]
413
+ end
414
+
415
+ if name == "v"
416
+ @current_xml << "</v>"
364
417
  end
365
418
 
366
- @current_scratch = nil
419
+ @current_text = nil
367
420
  @state = :result
368
421
  elsif name == "sg"
369
422
  # <sg> is emitted to delimit text that should be displayed
370
423
  # highlighted. We preserve it in field values.
371
- @current_scratch << "</sg>"
424
+ @current_xml << "</sg>"
372
425
  end
373
426
  end,
374
427
  :start_element => lambda do |name, attributes|
@@ -378,11 +431,12 @@ module Splunk
378
431
  "#{key}=\"#{value}\""
379
432
  end
380
433
  text = "<" + s.join(" ") + ">"
381
- @current_scratch << text
434
+ @current_xml << text
382
435
  end
383
436
  end,
384
437
  :characters => lambda do |text|
385
- @current_scratch << text
438
+ @current_text << text
439
+ @current_xml << Splunk::escape_string(text)
386
440
  end
387
441
  }
388
442
  }