splunk-sdk-ruby 0.1.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  }