splunk-sdk-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +160 -0
- data/Gemfile +8 -0
- data/LICENSE +177 -0
- data/README.md +310 -0
- data/Rakefile +40 -0
- data/examples/1_connect.rb +51 -0
- data/examples/2_manage.rb +103 -0
- data/examples/3_blocking_searches.rb +82 -0
- data/examples/4_asynchronous_searches.rb +79 -0
- data/examples/5_stream_data_to_splunk.rb +79 -0
- data/lib/splunk-sdk-ruby.rb +47 -0
- data/lib/splunk-sdk-ruby/ambiguous_entity_reference.rb +28 -0
- data/lib/splunk-sdk-ruby/atomfeed.rb +323 -0
- data/lib/splunk-sdk-ruby/collection.rb +417 -0
- data/lib/splunk-sdk-ruby/collection/apps.rb +35 -0
- data/lib/splunk-sdk-ruby/collection/case_insensitive_collection.rb +58 -0
- data/lib/splunk-sdk-ruby/collection/configuration_file.rb +50 -0
- data/lib/splunk-sdk-ruby/collection/configurations.rb +80 -0
- data/lib/splunk-sdk-ruby/collection/jobs.rb +136 -0
- data/lib/splunk-sdk-ruby/collection/messages.rb +51 -0
- data/lib/splunk-sdk-ruby/context.rb +522 -0
- data/lib/splunk-sdk-ruby/entity.rb +260 -0
- data/lib/splunk-sdk-ruby/entity/index.rb +191 -0
- data/lib/splunk-sdk-ruby/entity/job.rb +339 -0
- data/lib/splunk-sdk-ruby/entity/message.rb +36 -0
- data/lib/splunk-sdk-ruby/entity/saved_search.rb +71 -0
- data/lib/splunk-sdk-ruby/entity/stanza.rb +45 -0
- data/lib/splunk-sdk-ruby/entity_not_ready.rb +26 -0
- data/lib/splunk-sdk-ruby/illegal_operation.rb +27 -0
- data/lib/splunk-sdk-ruby/namespace.rb +239 -0
- data/lib/splunk-sdk-ruby/resultsreader.rb +716 -0
- data/lib/splunk-sdk-ruby/service.rb +339 -0
- data/lib/splunk-sdk-ruby/splunk_http_error.rb +49 -0
- data/lib/splunk-sdk-ruby/synonyms.rb +50 -0
- data/lib/splunk-sdk-ruby/version.rb +27 -0
- data/lib/splunk-sdk-ruby/xml_shim.rb +117 -0
- data/splunk-sdk-ruby.gemspec +27 -0
- data/test/atom_test_data.rb +472 -0
- data/test/data/atom/atom_feed_with_message.xml +19 -0
- data/test/data/atom/atom_with_feed.xml +99 -0
- data/test/data/atom/atom_with_several_entries.xml +101 -0
- data/test/data/atom/atom_with_simple_entries.xml +30 -0
- data/test/data/atom/atom_without_feed.xml +248 -0
- data/test/data/export/4.2.5/export_results.xml +88 -0
- data/test/data/export/4.3.5/export_results.xml +87 -0
- data/test/data/export/5.0.1/export_results.xml +78 -0
- data/test/data/export/5.0.1/nonreporting.xml +232 -0
- data/test/data/results/4.2.5/results-empty.xml +0 -0
- data/test/data/results/4.2.5/results-preview.xml +255 -0
- data/test/data/results/4.2.5/results.xml +336 -0
- data/test/data/results/4.3.5/results-empty.xml +0 -0
- data/test/data/results/4.3.5/results-preview.xml +1057 -0
- data/test/data/results/4.3.5/results.xml +626 -0
- data/test/data/results/5.0.2/results-empty.xml +1 -0
- data/test/data/results/5.0.2/results-empty_preview.xml +1 -0
- data/test/data/results/5.0.2/results-preview.xml +448 -0
- data/test/data/results/5.0.2/results.xml +501 -0
- data/test/export_test_data.json +360 -0
- data/test/resultsreader_test_data.json +1119 -0
- data/test/services.server.info.xml +43 -0
- data/test/services.xml +111 -0
- data/test/test_atomfeed.rb +71 -0
- data/test/test_collection.rb +278 -0
- data/test/test_configuration_file.rb +124 -0
- data/test/test_context.rb +119 -0
- data/test/test_entity.rb +95 -0
- data/test/test_helper.rb +250 -0
- data/test/test_http_error.rb +52 -0
- data/test/test_index.rb +91 -0
- data/test/test_jobs.rb +319 -0
- data/test/test_messages.rb +17 -0
- data/test/test_namespace.rb +188 -0
- data/test/test_restarts.rb +49 -0
- data/test/test_resultsreader.rb +106 -0
- data/test/test_roles.rb +41 -0
- data/test/test_saved_searches.rb +119 -0
- data/test/test_service.rb +65 -0
- data/test/test_users.rb +33 -0
- data/test/test_xml_shim.rb +28 -0
- data/test/testfile.txt +1 -0
- metadata +200 -0
@@ -0,0 +1,35 @@
|
|
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
|
+
##
|
18
|
+
# Provides a class representing a configuration file.
|
19
|
+
#
|
20
|
+
|
21
|
+
require_relative '../collection'
|
22
|
+
|
23
|
+
module Splunk
|
24
|
+
class Apps < Collection
|
25
|
+
def initialize(service, resource, entity_class=Entity)
|
26
|
+
super(service, resource, entity_class)
|
27
|
+
|
28
|
+
# On Splunk 4.2, a newly created app does not have its Atom returned.
|
29
|
+
# Instead, an Atom entity named "Created" is returned, so we have to
|
30
|
+
# refresh the app manually. After 4.2 is no longer supported, we can
|
31
|
+
# remove this line.
|
32
|
+
@always_fetch = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,58 @@
|
|
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
|
+
require_relative '../entity'
|
19
|
+
|
20
|
+
##
|
21
|
+
# Provides a class representing the collection of users and roles in Splunk.
|
22
|
+
# This should look identical to Collection to the end user of the SDK.
|
23
|
+
#
|
24
|
+
# Users and roles are both case insensitive to the entity name, and neither
|
25
|
+
# returns the newly created entity.
|
26
|
+
#
|
27
|
+
|
28
|
+
module Splunk
|
29
|
+
class CaseInsensitiveCollection < Collection
|
30
|
+
def initialize(service, resource, entity_class=Entity)
|
31
|
+
super(service, resource, entity_class)
|
32
|
+
|
33
|
+
# +CaseInsensitiveCollection+ is only currently used for users and roles,
|
34
|
+
# both of which require @+always_fetch=true+. This property is not inherent
|
35
|
+
# to +CaseInsensitiveCollection+ in any particular way. It was just a
|
36
|
+
# convenient place to put it.
|
37
|
+
@always_fetch = true
|
38
|
+
end
|
39
|
+
|
40
|
+
# The following methods only downcase the name they are passed, and should
|
41
|
+
# be invisible to the user.
|
42
|
+
def create(name, args={}) # :nodoc:
|
43
|
+
super(name.downcase(), args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(name, namespace=nil) # :nodoc:
|
47
|
+
super(name.downcase(), namespace)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch(name, namespace=nil) # :nodoc:
|
51
|
+
super(name.downcase(), namespace)
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_key?(name) # :nodoc:
|
55
|
+
super(name.downcase())
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
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
|
+
##
|
18
|
+
# Provides a class representing a configuration file.
|
19
|
+
#
|
20
|
+
|
21
|
+
require_relative '../collection'
|
22
|
+
|
23
|
+
module Splunk
|
24
|
+
##
|
25
|
+
# +ConfigurationFile+ is a collection containing configuration stanzas.
|
26
|
+
#
|
27
|
+
# This class's API is identical to +Collection+, so a user should not
|
28
|
+
# have to be aware of its existence.
|
29
|
+
#
|
30
|
+
class ConfigurationFile < Collection # :nodoc:
|
31
|
+
# This class is unusual: it is the element of a collection itself,
|
32
|
+
# and its elements are entities.
|
33
|
+
|
34
|
+
def initialize(service, name, namespace=nil)
|
35
|
+
super(service, ["configs", "conf-#{name}"], entity_class=Stanza)
|
36
|
+
@name = name
|
37
|
+
@namespace = namespace || service.namespace
|
38
|
+
end
|
39
|
+
|
40
|
+
def create(name, args={})
|
41
|
+
body_args = args.clone()
|
42
|
+
if !args.has_key?(:namespace)
|
43
|
+
body_args[:namespace] = @namespace
|
44
|
+
end
|
45
|
+
super(name, body_args)
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :name
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,80 @@
|
|
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
|
+
##
|
18
|
+
# Provides +Configurations+, a collection of configuration files in Splunk.
|
19
|
+
# +Configurations+ has an API identical to its superclass, +Collection+,
|
20
|
+
# so a user of the SDK should not have to be aware of its existance.
|
21
|
+
#
|
22
|
+
|
23
|
+
require_relative '../collection'
|
24
|
+
require_relative 'configuration_file'
|
25
|
+
|
26
|
+
module Splunk
|
27
|
+
##
|
28
|
+
# Class representing a collection of configuration files.
|
29
|
+
#
|
30
|
+
# The API of +Configurations+ is identical to +Collection+,
|
31
|
+
# so the user should not need to be aware of this class.
|
32
|
+
#
|
33
|
+
class Configurations < Collection # :nodoc:
|
34
|
+
def initialize(service)
|
35
|
+
super(service, PATH_CONFS, entity_class=ConfigurationFile)
|
36
|
+
end
|
37
|
+
|
38
|
+
def atom_entry_to_entity(entry)
|
39
|
+
name = entry["title"]
|
40
|
+
return ConfigurationFile.new(@service, name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def create(name, args={})
|
44
|
+
# Don't bother catching the response. It either succeeds and returns
|
45
|
+
# an empty body, or fails and throws a +SplunkHTTPError+.
|
46
|
+
request_args = {:method => :POST,
|
47
|
+
:resource => PATH_CONFS,
|
48
|
+
:body => {"__conf" => name}}
|
49
|
+
if args.has_key?(:namespace)
|
50
|
+
request_args[:namespace] = args[:namespace]
|
51
|
+
end
|
52
|
+
@service.request(request_args)
|
53
|
+
return ConfigurationFile.new(@service, name,
|
54
|
+
args[:namespace] || @service.namespace)
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(name)
|
58
|
+
raise IllegalOperation.new("Cannot delete configuration files from" +
|
59
|
+
" the REST API.")
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch(name)
|
63
|
+
begin
|
64
|
+
# Make a request to the server to see if _name_ exists.
|
65
|
+
# We don't actually use any information returned from the server
|
66
|
+
# besides the status code.
|
67
|
+
request_args = {:resource => PATH_CONFS + [name]}
|
68
|
+
@service.request(request_args)
|
69
|
+
|
70
|
+
return ConfigurationFile.new(@service, name)
|
71
|
+
rescue SplunkHTTPError => err
|
72
|
+
if err.code == 404
|
73
|
+
return nil
|
74
|
+
else
|
75
|
+
raise err
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -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 'delegate'
|
18
|
+
|
19
|
+
require_relative '../collection'
|
20
|
+
require_relative '../entity/job'
|
21
|
+
|
22
|
+
##
|
23
|
+
# Provides a class representing the collection of jobs in Splunk.
|
24
|
+
#
|
25
|
+
|
26
|
+
module Splunk
|
27
|
+
##
|
28
|
+
# Class representing a search job in Splunk.
|
29
|
+
#
|
30
|
+
# +Jobs+ adds two additional methods to +Collection+ to start additional
|
31
|
+
# kinds of search job. The basic +create+ method starts a normal,
|
32
|
+
# asynchronous search job. The two new methods, +create_oneshot+ and
|
33
|
+
# +create_stream+, creating oneshot and streaming searches, respectively,
|
34
|
+
# which block until the search finishes and return the results directly.
|
35
|
+
#
|
36
|
+
class Jobs < Collection
|
37
|
+
def initialize(service)
|
38
|
+
super(service, PATH_JOBS, entity_class=Job)
|
39
|
+
|
40
|
+
# +Jobs+ is one of the inconsistent collections where 0 means
|
41
|
+
# list all, not -1.
|
42
|
+
@infinite_count = 0
|
43
|
+
end
|
44
|
+
|
45
|
+
def atom_entry_to_entity(entry) # :nodoc:
|
46
|
+
sid = entry["content"]["sid"]
|
47
|
+
return Job.new(@service, sid)
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Creates an asynchronous search job.
|
52
|
+
#
|
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].
|
55
|
+
#
|
56
|
+
def create(query, args={})
|
57
|
+
if args.has_key?(:exec_mode)
|
58
|
+
raise ArgumentError.new("Cannot specify exec_mode for create. Use " +
|
59
|
+
"create_oneshot or create_stream instead.")
|
60
|
+
end
|
61
|
+
|
62
|
+
args['search'] = query
|
63
|
+
response = @service.request(:method => :POST,
|
64
|
+
:resource => @resource,
|
65
|
+
:body => args)
|
66
|
+
sid = Splunk::text_at_xpath("/response/sid", response.body)
|
67
|
+
Job.new(@service, sid)
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Creates a blocking search.
|
72
|
+
#
|
73
|
+
# The +create_oneshot+ method starts a search _query_, and any optional
|
74
|
+
# arguments specified in a hash (which are identical to those taken by
|
75
|
+
# +create+). It then blocks until the job finished, and returns the
|
76
|
+
# results, as transformed by any transforming search commands in _query_
|
77
|
+
# (equivalent to calling the +results+ method on a +Job+).
|
78
|
+
#
|
79
|
+
# Returns: a stream readable by +ResultsReader+.
|
80
|
+
#
|
81
|
+
def create_oneshot(query, args={})
|
82
|
+
args[:search] = query
|
83
|
+
args[:exec_mode] = 'oneshot'
|
84
|
+
response = @service.request(:method => :POST,
|
85
|
+
:resource => @resource,
|
86
|
+
:body => args)
|
87
|
+
return response.body
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Creates a blocking search without transforming search commands.
|
92
|
+
#
|
93
|
+
# The +create_export+ method starts a search _query_, and any optional
|
94
|
+
# arguments specified in a hash (which are identical to those taken by
|
95
|
+
# +create+). It then blocks until the job is finished, and returns the
|
96
|
+
# events found by the job before any transforming search commands
|
97
|
+
# (equivalent to calling +events+ on a +Job+).
|
98
|
+
#
|
99
|
+
# Returns: a stream readable by +MultiResultsReader+.
|
100
|
+
#
|
101
|
+
def create_export(query, args={})
|
102
|
+
args["search"] = query
|
103
|
+
response = @service.request(:method => :GET,
|
104
|
+
:resource => @resource + ["export"],
|
105
|
+
:query => args)
|
106
|
+
return ExportStream.new(response.body)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Deprecated.
|
110
|
+
def create_stream(query, args={}) # :nodoc:
|
111
|
+
warn "[DEPRECATION] Jobs#create_stream is deprecated. Use Jobs#create_export instead."
|
112
|
+
create_export(query, args)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Marks streams returned by the export endpoint for special handling.
|
118
|
+
#
|
119
|
+
# ResultsReader is supposed to handle streams from export differently
|
120
|
+
# from those from other endpoints, so we use this delegator to mark them.
|
121
|
+
#
|
122
|
+
class ExportStream < Delegator
|
123
|
+
def initialize(obj)
|
124
|
+
super # pass obj to Delegator constructor, required
|
125
|
+
@delegate = obj # store obj for future use
|
126
|
+
end
|
127
|
+
|
128
|
+
def __getobj__()
|
129
|
+
@delegate
|
130
|
+
end
|
131
|
+
|
132
|
+
def __setobj__(obj)
|
133
|
+
@delegate = obj
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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
|
+
# Provides a collection representing system-wide messages on Splunk.
|
21
|
+
#
|
22
|
+
|
23
|
+
module Splunk
|
24
|
+
##
|
25
|
+
# Collection representing system-wide messages on Splunk.
|
26
|
+
#
|
27
|
+
# There is no API difference from +Collection+, and so no reason
|
28
|
+
# for a user to be aware of this class.
|
29
|
+
#
|
30
|
+
class Messages < Collection # :nodoc:
|
31
|
+
def create(name, args)
|
32
|
+
body_args = args.clone()
|
33
|
+
body_args["name"] = name
|
34
|
+
|
35
|
+
request_args = {
|
36
|
+
:method => :POST,
|
37
|
+
:resource => @resource,
|
38
|
+
:body => body_args
|
39
|
+
}
|
40
|
+
if args.has_key?(:namespace)
|
41
|
+
request_args[:namespace] = body_args.delete(:namespace)
|
42
|
+
end
|
43
|
+
|
44
|
+
response = @service.request(request_args)
|
45
|
+
entity = Message.new(@service, Splunk::namespace(:sharing => "system"),
|
46
|
+
@resource, name)
|
47
|
+
return entity
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,522 @@
|
|
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
|
+
##
|
18
|
+
# Provides the +Context+ class, the basic class representing a connection to a
|
19
|
+
# Splunk server. +Context+ is minimal, and only handles authentication and calls
|
20
|
+
# to the REST API. For most uses, you will want to use its subclass +Service+,
|
21
|
+
# which adds convenient methods to access the various collections and entities
|
22
|
+
# on Splunk.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'net/http'
|
26
|
+
|
27
|
+
require_relative 'splunk_http_error'
|
28
|
+
require_relative 'version'
|
29
|
+
require_relative 'xml_shim'
|
30
|
+
require_relative 'namespace'
|
31
|
+
|
32
|
+
module Splunk
|
33
|
+
DEFAULT_HOST = 'localhost'
|
34
|
+
DEFAULT_PORT = 8089
|
35
|
+
DEFAULT_SCHEME = :https
|
36
|
+
|
37
|
+
# Class encapsulating a connection to a Splunk server.
|
38
|
+
#
|
39
|
+
# This class is used for lower-level REST-based control of Splunk.
|
40
|
+
# For most use, you will want to use +Context+'s subclass +Service+, which
|
41
|
+
# provides convenient access to Splunk's various collections and entities.
|
42
|
+
#
|
43
|
+
# To use the +Context+ class, create a new +Context+ with a hash of arguments
|
44
|
+
# giving the details of the connection, and call the +login+ method on it:
|
45
|
+
#
|
46
|
+
# context = Splunk::Context.new(:username => "admin",
|
47
|
+
# :password => "changeme").login()
|
48
|
+
#
|
49
|
+
# +Context+#+new+ takes a hash of keyword arguments. The keys it understands
|
50
|
+
# are:
|
51
|
+
#
|
52
|
+
# * +:username+ - log in to Splunk as this user (no default)
|
53
|
+
# * +:password+ - password to use when logging in (no default)
|
54
|
+
# * +:host+ - Splunk host (e.g. "10.1.2.3") (default: 'localhost')
|
55
|
+
# * +:port+ - the Splunk management port (default: 8089)
|
56
|
+
# * +:protocol+ - either :https or :http (default: :https)
|
57
|
+
# * +:namespace+ - a +Namespace+ object representing the default namespace for
|
58
|
+
# this context (default: +DefaultNamespace+)
|
59
|
+
# * +:token+ - a preauthenticated Splunk token (default: +nil+)
|
60
|
+
#
|
61
|
+
# If you specify a token, you need not specify a username or password, nor
|
62
|
+
# do you need to call the +login+ method.
|
63
|
+
#
|
64
|
+
# +Context+ provides three other important methods:
|
65
|
+
#
|
66
|
+
# * +connect+ opens a socket to the Splunk server.
|
67
|
+
# * +request+ issues a request to the REST API.
|
68
|
+
# * +restart+ restarts the Splunk server and handles waiting for it to come
|
69
|
+
# back up.
|
70
|
+
#
|
71
|
+
class Context
|
72
|
+
def initialize(args)
|
73
|
+
@token = args.fetch(:token, nil)
|
74
|
+
@scheme = args.fetch(:scheme, DEFAULT_SCHEME).intern()
|
75
|
+
@host = args.fetch(:host, DEFAULT_HOST)
|
76
|
+
@port = Integer(args.fetch(:port, DEFAULT_PORT))
|
77
|
+
@username = args.fetch(:username, nil)
|
78
|
+
@password = args.fetch(:password, nil)
|
79
|
+
# Have to use Splunk::namespace() or we will call the
|
80
|
+
# local accessor.
|
81
|
+
@namespace = args.fetch(:namespace,
|
82
|
+
Splunk::namespace(:sharing => "default"))
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# The protocol used to connect.
|
87
|
+
#
|
88
|
+
# Defaults to +:https+.
|
89
|
+
#
|
90
|
+
# Returns: +:http+ or +:https+.
|
91
|
+
#
|
92
|
+
attr_reader :scheme
|
93
|
+
|
94
|
+
##
|
95
|
+
# The host to connect to.
|
96
|
+
#
|
97
|
+
# Defaults to "+localhost+".
|
98
|
+
#
|
99
|
+
# Returns: a +String+.
|
100
|
+
#
|
101
|
+
attr_reader :host
|
102
|
+
|
103
|
+
##
|
104
|
+
# The port to connect to.
|
105
|
+
#
|
106
|
+
# Defaults to +8089+.
|
107
|
+
#
|
108
|
+
# Returns: an +Integer+.
|
109
|
+
#
|
110
|
+
attr_reader :port
|
111
|
+
|
112
|
+
##
|
113
|
+
# The authentication token on Splunk.
|
114
|
+
#
|
115
|
+
# If this +Context+ is not logged in, this is +nil+. Otherwise it is a
|
116
|
+
# +String+ that is passed with each request.
|
117
|
+
#
|
118
|
+
# Returns: a +String+ or +nil+.
|
119
|
+
#
|
120
|
+
attr_reader :token
|
121
|
+
|
122
|
+
##
|
123
|
+
# The username used to connect.
|
124
|
+
#
|
125
|
+
# If a token is provided, this field can be +nil+.
|
126
|
+
#
|
127
|
+
# Returns: a +String+ or +nil+.
|
128
|
+
#
|
129
|
+
attr_reader :username
|
130
|
+
|
131
|
+
##
|
132
|
+
# The password used to connect.
|
133
|
+
#
|
134
|
+
# If a token is provided, this field can be +nil+.
|
135
|
+
#
|
136
|
+
# Returns: a +String+ or +nil+.
|
137
|
+
#
|
138
|
+
attr_reader :password
|
139
|
+
|
140
|
+
##
|
141
|
+
# The default namespace used for requests on this +Context+.
|
142
|
+
#
|
143
|
+
# The namespace must be a +Namespace+ object. If a call to +request+ is
|
144
|
+
# made without a namespace, this namespace is used for the request.
|
145
|
+
#
|
146
|
+
# Defaults to +DefaultNamespace+.
|
147
|
+
#
|
148
|
+
# Returns: a +Namespace+ object.
|
149
|
+
#
|
150
|
+
attr_reader :namespace
|
151
|
+
|
152
|
+
##
|
153
|
+
# Opens a TCP socket to the Splunk HTTP server.
|
154
|
+
#
|
155
|
+
# If the +scheme+ field of this +Context+ is +:https+, this method returns
|
156
|
+
# an +SSLSocket+. If +scheme+ is +:http+, a +TCPSocket+ is returned. Due to
|
157
|
+
# design errors in Ruby's standard library, these two do not share the same
|
158
|
+
# method names, so code written for HTTPS will not work for HTTP.
|
159
|
+
#
|
160
|
+
# Returns: an +SSLSocket+ or +TCPSocket+.
|
161
|
+
#
|
162
|
+
def connect()
|
163
|
+
socket = TCPSocket.new(@host, @port)
|
164
|
+
if scheme == :https
|
165
|
+
ssl_context = OpenSSL::SSL::SSLContext.new()
|
166
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
167
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
168
|
+
ssl_socket.sync_close = true
|
169
|
+
ssl_socket.connect()
|
170
|
+
return ssl_socket
|
171
|
+
else
|
172
|
+
return socket
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Logs into Splunk and set the token field on this +Context+.
|
178
|
+
#
|
179
|
+
# The +login+ method assumes that the +Context+ has a username and password
|
180
|
+
# set. You cannot pass them as arguments to this method. On a successful
|
181
|
+
# login, the token field of the +Context+ is set to the token returned by
|
182
|
+
# Splunk, and all further requests to the server will send this token.
|
183
|
+
#
|
184
|
+
# If this +Context+ already has a token that is not +nil+, it is already
|
185
|
+
# logged in, and this method is a nop.
|
186
|
+
#
|
187
|
+
# Raises +SplunkHTTPError+ if there is a problem logging in.
|
188
|
+
#
|
189
|
+
# Returns: the +Context+.
|
190
|
+
#
|
191
|
+
def login()
|
192
|
+
if @token # If we're already logged in, this method is a nop.
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
196
|
+
response = request(:namespace => Splunk::namespace(:sharing => "default"),
|
197
|
+
:method => :POST,
|
198
|
+
:resource => ["auth", "login"],
|
199
|
+
:query => {},
|
200
|
+
:headers => {},
|
201
|
+
:body => {:username=>@username, :password=>@password})
|
202
|
+
# The response looks like:
|
203
|
+
# <response>
|
204
|
+
# <sessionKey>da950729652f8255c230afe37bdf8b97</sessionKey>
|
205
|
+
# </response>
|
206
|
+
@token = Splunk::text_at_xpath("//sessionKey", response.body)
|
207
|
+
|
208
|
+
self
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Logs out of Splunk.
|
213
|
+
#
|
214
|
+
# This sets the @token attribute to +nil+.
|
215
|
+
#
|
216
|
+
# Returns: the +Context+.
|
217
|
+
#
|
218
|
+
def logout()
|
219
|
+
@token = nil
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Issues an HTTP(S) request to the Splunk instance.
|
225
|
+
#
|
226
|
+
# The +request+ method does not take a URL. Instead, it takes a hash of
|
227
|
+
# optional arguments specifying an action in the REST API. This avoids the
|
228
|
+
# problem knowing whether a given piece of data is URL encoded or not.
|
229
|
+
#
|
230
|
+
# The arguments are:
|
231
|
+
#
|
232
|
+
# * +method+: The HTTP method to use (one of +:GET+, +:POST+, or +:DELETE+;
|
233
|
+
# default: +:GET+).
|
234
|
+
# * +namespace+: The namespace to request a resource from Splunk in. Must
|
235
|
+
# be a +Namespace+ object. (default: the value of +@namespace+ on
|
236
|
+
# this +Context+)
|
237
|
+
# * +resource+: An array of strings specifying the components of the path
|
238
|
+
# to the resource after the namespace. The strings should not be URL
|
239
|
+
# encoded, as that will be handled by +request+. (default: [])
|
240
|
+
# * +query+: A hash containing the values to be encoded as
|
241
|
+
# the query (the part following +?+) in the URL. Nothing should be URL
|
242
|
+
# encoded as +request+ will do the encoding. If you need to pass multiple
|
243
|
+
# values for the same key, insert them as an Array as the value of their
|
244
|
+
# key into the Hash, and they will be properly encoded as a sequence of
|
245
|
+
# entries with the same key. (default: {})
|
246
|
+
# * +headers+: A hash containing the values to be encoded as headers. None
|
247
|
+
# should be URL encoded, and the +request+ method will automatically add
|
248
|
+
# headers for +User-Agent+ and Splunk authentication for you. Keys must
|
249
|
+
# be unique, so the values must be strings, not arrays, unlike for
|
250
|
+
# +query+. (default: {})
|
251
|
+
# * +body+: Either a hash to be encoded as the body of a POST request, or
|
252
|
+
# a string to be used as the raw, already encoded body of a POST request.
|
253
|
+
# If you pass a hash, you can pass multiple values for the same key by
|
254
|
+
# encoding them as an Array, which will be properly set as multiple
|
255
|
+
# instances of the same key in the POST body. Nothing in the hash should
|
256
|
+
# be URL encoded, as +request+ will handle all such encoding.
|
257
|
+
# (default: {})
|
258
|
+
#
|
259
|
+
# If Splunk responds with an HTTP code 2xx, the +request+ method returns
|
260
|
+
# an HTTP response object (the import methods of which are +code+,
|
261
|
+
# +message+, and +body+, and +each+ to enumerate over the response
|
262
|
+
# headers). If the HTTP code is not 2xx, +request+ raises a
|
263
|
+
# +SplunkHTTPError+.
|
264
|
+
#
|
265
|
+
# *Examples:*
|
266
|
+
#
|
267
|
+
# c = Splunk::connect(username="admin", password="changeme")
|
268
|
+
# # Get a list of the indexes in this Splunk instance.
|
269
|
+
# c.request(:namespace => Splunk::namespace(),
|
270
|
+
# :resource => ["data", "indexes"])
|
271
|
+
# # Create a new index called "my_new_index"
|
272
|
+
# c.request(:method => :POST,
|
273
|
+
# :resource => ["data", "indexes"],
|
274
|
+
# :body => {"name", "my_new_index"})
|
275
|
+
#
|
276
|
+
def request(args)
|
277
|
+
method = args.fetch(:method, :GET)
|
278
|
+
scheme = @scheme
|
279
|
+
host = @host
|
280
|
+
port = @port
|
281
|
+
namespace = args.fetch(:namespace, @namespace)
|
282
|
+
resource = args.fetch(:resource, [])
|
283
|
+
query = args.fetch(:query, {})
|
284
|
+
headers = args.fetch(:headers, {})
|
285
|
+
body = args.fetch(:body, {})
|
286
|
+
|
287
|
+
if method != :GET && method != :POST && method != :DELETE
|
288
|
+
raise ArgumentError.new("Method must be one of :GET, :POST, or " +
|
289
|
+
":DELETE, found: #{method}")
|
290
|
+
end
|
291
|
+
|
292
|
+
if scheme && scheme != :http && scheme != :https
|
293
|
+
raise ArgumentError.new("Scheme must be one of :http or :https, " +
|
294
|
+
"found: #{scheme}")
|
295
|
+
end
|
296
|
+
|
297
|
+
if port && !port.is_a?(Integer)
|
298
|
+
raise ArgumentError.new("Port must be an Integer, found: #{port}")
|
299
|
+
end
|
300
|
+
|
301
|
+
if !namespace.is_a?(Namespace)
|
302
|
+
raise ArgumentError.new("Namespace must be a Namespace, " +
|
303
|
+
"found: #{namespace}")
|
304
|
+
end
|
305
|
+
|
306
|
+
# Construct the URL for the request.
|
307
|
+
url = ""
|
308
|
+
url << "#{(scheme || @scheme).to_s}://"
|
309
|
+
url << "#{host || @host}:#{(port || @port).to_s}/"
|
310
|
+
url << (namespace.to_path_fragment() + resource).
|
311
|
+
map {|fragment| URI::encode(fragment)}.
|
312
|
+
join("/")
|
313
|
+
|
314
|
+
return request_by_url(:url => url,
|
315
|
+
:method => method,
|
316
|
+
:query => query,
|
317
|
+
:headers => headers,
|
318
|
+
:body => body)
|
319
|
+
end
|
320
|
+
|
321
|
+
##
|
322
|
+
# Makes a request to the Splunk server given a prebuilt URL.
|
323
|
+
#
|
324
|
+
# Unless you are using a URL that was returned by the Splunk server
|
325
|
+
# as part of an Atom feed, you should prefer the +request+ method, which
|
326
|
+
# has much clearer semantics.
|
327
|
+
#
|
328
|
+
# The +request_by_url+ method takes a hash of arguments. The recognized
|
329
|
+
# arguments are:
|
330
|
+
#
|
331
|
+
# * +:url+: (a +URI+ object or a +String+) The URL, including authority, to
|
332
|
+
# make a request to.
|
333
|
+
# * +:method+: (+:GET+, +:POST+, or +:DELETE+) The HTTP method to use.
|
334
|
+
# * +query+: A hash containing the values to be encoded as
|
335
|
+
# the query (the part following +?+) in the URL. Nothing should be URL
|
336
|
+
# encoded as +request+ will do the encoding. If you need to pass multiple
|
337
|
+
# values for the same key, insert them as an +Array+ as the value of their
|
338
|
+
# key into the Hash, and they will be properly encoded as a sequence of
|
339
|
+
# entries with the same key. (default: {})
|
340
|
+
# * +headers+: A hash containing the values to be encoded as headers. None
|
341
|
+
# should be URL encoded, and the +request+ method will automatically add
|
342
|
+
# headers for +User-Agent+ and Splunk authentication for you. Keys must
|
343
|
+
# be unique, so the values must be strings, not arrays, unlike for
|
344
|
+
# +query+. (default: {})
|
345
|
+
# * +body+: Either a hash to be encoded as the body of a POST request, or
|
346
|
+
# a string to be used as the raw, already encoded body of a POST request.
|
347
|
+
# If you pass a hash, you can pass multiple values for the same key by
|
348
|
+
# encoding them as an +Array+, which will be properly set as multiple
|
349
|
+
# instances of the same key in the POST body. Nothing in the hash should
|
350
|
+
# be URL encoded, as +request+ will handle all such encoding.
|
351
|
+
# (default: {})
|
352
|
+
#
|
353
|
+
# If Splunk responds with an HTTP code 2xx, the +request_by_url+ method
|
354
|
+
# returns an HTTP response object (the import methods of which are +code+,
|
355
|
+
# +message+, and +body+, and +each+ to enumerate over the response
|
356
|
+
# headers). If the HTTP code is not 2xx, the +request_by_url+ method
|
357
|
+
# raises a +SplunkHTTPError+.
|
358
|
+
#
|
359
|
+
def request_by_url(args)
|
360
|
+
url = args.fetch(:url)
|
361
|
+
if url.is_a?(String)
|
362
|
+
url = URI(url)
|
363
|
+
end
|
364
|
+
method = args.fetch(:method, :GET)
|
365
|
+
query = args.fetch(:query, {})
|
366
|
+
headers = args.fetch(:headers, {})
|
367
|
+
body = args.fetch(:body, {})
|
368
|
+
|
369
|
+
if !query.empty?
|
370
|
+
url.query = URI.encode_www_form(query)
|
371
|
+
end
|
372
|
+
|
373
|
+
if method == :GET
|
374
|
+
request = Net::HTTP::Get.new(url.request_uri)
|
375
|
+
elsif method == :POST
|
376
|
+
request = Net::HTTP::Post.new(url.request_uri)
|
377
|
+
elsif method == :DELETE
|
378
|
+
request = Net::HTTP::Delete.new(url.request_uri)
|
379
|
+
end
|
380
|
+
|
381
|
+
# Headers
|
382
|
+
request["User-Agent"] = "splunk-sdk-ruby/#{VERSION}"
|
383
|
+
request["Authorization"] = "Splunk #{@token}" if @token
|
384
|
+
headers.each_entry do |key, value|
|
385
|
+
request[key] = value
|
386
|
+
end
|
387
|
+
|
388
|
+
# Body
|
389
|
+
if body.is_a?(String)
|
390
|
+
# This case exists only for submitting an event to an index via HTTP.
|
391
|
+
request.body = body
|
392
|
+
else
|
393
|
+
request.body = URI.encode_www_form(body)
|
394
|
+
end
|
395
|
+
|
396
|
+
# Issue the request.
|
397
|
+
response = Net::HTTP::start(
|
398
|
+
url.hostname, url.port,
|
399
|
+
:use_ssl => url.scheme == 'https',
|
400
|
+
# We don't support certificates.
|
401
|
+
:verify_mode => OpenSSL::SSL::VERIFY_NONE
|
402
|
+
) do |http|
|
403
|
+
http.request(request)
|
404
|
+
end
|
405
|
+
|
406
|
+
# Handle any errors.
|
407
|
+
if !response.is_a?(Net::HTTPSuccess)
|
408
|
+
raise SplunkHTTPError.new(response)
|
409
|
+
else
|
410
|
+
return response
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
##
|
415
|
+
# Restarts this Splunk instance.
|
416
|
+
#
|
417
|
+
# The +restart+ method may be called with an optional timeout. If you pass
|
418
|
+
# a timeout, +restart+ will wait up to that number of seconds for the
|
419
|
+
# server to come back up before returning. If +restart+ did not time out,
|
420
|
+
# it leaves the +Context+ logged in when it returns.
|
421
|
+
#
|
422
|
+
# If the timeout is, omitted, the +restart+ method returns immediately, and
|
423
|
+
# you will have to ascertain if Splunk has come back up yourself, for
|
424
|
+
# example with code like:
|
425
|
+
#
|
426
|
+
# context = Context.new(...).login()
|
427
|
+
# context.restart()
|
428
|
+
# Timeout::timeout(timeout) do
|
429
|
+
# while !context.server_accepting_connections? ||
|
430
|
+
# context.server_requires_restart?
|
431
|
+
# sleep(0.3)
|
432
|
+
# end
|
433
|
+
# end
|
434
|
+
#
|
435
|
+
# Returns: this +Context+.
|
436
|
+
#
|
437
|
+
def restart(timeout=nil)
|
438
|
+
# Set a message saying that restart is required. Otherwise we have no
|
439
|
+
# way of knowing if Splunk has actually gone down for a restart or not.
|
440
|
+
request(:method => :POST,
|
441
|
+
:namespace => Splunk::namespace(:sharing => "default"),
|
442
|
+
:resource => ["messages"],
|
443
|
+
:body => {"name" => "restart_required",
|
444
|
+
"value" => "Message set by restart method" +
|
445
|
+
" of the Splunk Ruby SDK"})
|
446
|
+
|
447
|
+
# Make the actual restart request.
|
448
|
+
request(:resource => ["server", "control", "restart"])
|
449
|
+
|
450
|
+
# Clear our old token, which will no longer work after the restart.
|
451
|
+
logout()
|
452
|
+
|
453
|
+
# If +timeout+ is +nil+, return immediately. If timeout is a positive
|
454
|
+
# integer, wait for +timeout+ seconds for the server to come back up.
|
455
|
+
if !timeout.nil?
|
456
|
+
Timeout::timeout(timeout) do
|
457
|
+
while !server_accepting_connections? || server_requires_restart?
|
458
|
+
sleep(0.3)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Return the +Context+.
|
464
|
+
self
|
465
|
+
end
|
466
|
+
|
467
|
+
##
|
468
|
+
# Is the Splunk server accepting connections?
|
469
|
+
#
|
470
|
+
# Returns +true+ if the Splunk server is up and accepting REST API
|
471
|
+
# connections; +false+ otherwise.
|
472
|
+
#
|
473
|
+
def server_accepting_connections?()
|
474
|
+
begin
|
475
|
+
# Can't use login, since it has short circuits
|
476
|
+
# when @token != nil on the Context. Instead, make
|
477
|
+
# a request directly.
|
478
|
+
request(:resource => ["data", "indexes"])
|
479
|
+
rescue Errno::ECONNREFUSED, EOFError, Errno::ECONNRESET
|
480
|
+
return false
|
481
|
+
rescue SplunkHTTPError
|
482
|
+
# Splunk is up, because it responded with a proper HTTP error
|
483
|
+
# that our SplunkHTTPError parser understood.
|
484
|
+
return true
|
485
|
+
else
|
486
|
+
# Or the request worked, so we know that Splunk is up.
|
487
|
+
return true
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
##
|
492
|
+
# Is the Splunk server in a state requiring a restart?
|
493
|
+
#
|
494
|
+
# Returns +true+ if the Splunk server is down (equivalent to
|
495
|
+
# +server_accepting_connections?+), or if there is a +restart_required+
|
496
|
+
# message on the server; +false+ otherwise.
|
497
|
+
#
|
498
|
+
def server_requires_restart?()
|
499
|
+
begin # We must have two layers of rescue, because the login in the
|
500
|
+
# SplunkHTTPError rescue can also throw Errno::ECONNREFUSED.
|
501
|
+
begin
|
502
|
+
request(:resource => ["messages", "restart_required"])
|
503
|
+
return true
|
504
|
+
rescue SplunkHTTPError => err
|
505
|
+
if err.code == 401
|
506
|
+
# The messages endpoint requires authentication.
|
507
|
+
logout()
|
508
|
+
login()
|
509
|
+
return server_requires_restart?()
|
510
|
+
elsif err.code == 404
|
511
|
+
return false
|
512
|
+
else
|
513
|
+
raise err
|
514
|
+
end
|
515
|
+
end
|
516
|
+
rescue Errno::ECONNREFUSED, EOFError, Errno::ECONNRESET
|
517
|
+
return true
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
end
|
522
|
+
end
|