splunk-sdk-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/CHANGELOG.md +160 -0
  2. data/Gemfile +8 -0
  3. data/LICENSE +177 -0
  4. data/README.md +310 -0
  5. data/Rakefile +40 -0
  6. data/examples/1_connect.rb +51 -0
  7. data/examples/2_manage.rb +103 -0
  8. data/examples/3_blocking_searches.rb +82 -0
  9. data/examples/4_asynchronous_searches.rb +79 -0
  10. data/examples/5_stream_data_to_splunk.rb +79 -0
  11. data/lib/splunk-sdk-ruby.rb +47 -0
  12. data/lib/splunk-sdk-ruby/ambiguous_entity_reference.rb +28 -0
  13. data/lib/splunk-sdk-ruby/atomfeed.rb +323 -0
  14. data/lib/splunk-sdk-ruby/collection.rb +417 -0
  15. data/lib/splunk-sdk-ruby/collection/apps.rb +35 -0
  16. data/lib/splunk-sdk-ruby/collection/case_insensitive_collection.rb +58 -0
  17. data/lib/splunk-sdk-ruby/collection/configuration_file.rb +50 -0
  18. data/lib/splunk-sdk-ruby/collection/configurations.rb +80 -0
  19. data/lib/splunk-sdk-ruby/collection/jobs.rb +136 -0
  20. data/lib/splunk-sdk-ruby/collection/messages.rb +51 -0
  21. data/lib/splunk-sdk-ruby/context.rb +522 -0
  22. data/lib/splunk-sdk-ruby/entity.rb +260 -0
  23. data/lib/splunk-sdk-ruby/entity/index.rb +191 -0
  24. data/lib/splunk-sdk-ruby/entity/job.rb +339 -0
  25. data/lib/splunk-sdk-ruby/entity/message.rb +36 -0
  26. data/lib/splunk-sdk-ruby/entity/saved_search.rb +71 -0
  27. data/lib/splunk-sdk-ruby/entity/stanza.rb +45 -0
  28. data/lib/splunk-sdk-ruby/entity_not_ready.rb +26 -0
  29. data/lib/splunk-sdk-ruby/illegal_operation.rb +27 -0
  30. data/lib/splunk-sdk-ruby/namespace.rb +239 -0
  31. data/lib/splunk-sdk-ruby/resultsreader.rb +716 -0
  32. data/lib/splunk-sdk-ruby/service.rb +339 -0
  33. data/lib/splunk-sdk-ruby/splunk_http_error.rb +49 -0
  34. data/lib/splunk-sdk-ruby/synonyms.rb +50 -0
  35. data/lib/splunk-sdk-ruby/version.rb +27 -0
  36. data/lib/splunk-sdk-ruby/xml_shim.rb +117 -0
  37. data/splunk-sdk-ruby.gemspec +27 -0
  38. data/test/atom_test_data.rb +472 -0
  39. data/test/data/atom/atom_feed_with_message.xml +19 -0
  40. data/test/data/atom/atom_with_feed.xml +99 -0
  41. data/test/data/atom/atom_with_several_entries.xml +101 -0
  42. data/test/data/atom/atom_with_simple_entries.xml +30 -0
  43. data/test/data/atom/atom_without_feed.xml +248 -0
  44. data/test/data/export/4.2.5/export_results.xml +88 -0
  45. data/test/data/export/4.3.5/export_results.xml +87 -0
  46. data/test/data/export/5.0.1/export_results.xml +78 -0
  47. data/test/data/export/5.0.1/nonreporting.xml +232 -0
  48. data/test/data/results/4.2.5/results-empty.xml +0 -0
  49. data/test/data/results/4.2.5/results-preview.xml +255 -0
  50. data/test/data/results/4.2.5/results.xml +336 -0
  51. data/test/data/results/4.3.5/results-empty.xml +0 -0
  52. data/test/data/results/4.3.5/results-preview.xml +1057 -0
  53. data/test/data/results/4.3.5/results.xml +626 -0
  54. data/test/data/results/5.0.2/results-empty.xml +1 -0
  55. data/test/data/results/5.0.2/results-empty_preview.xml +1 -0
  56. data/test/data/results/5.0.2/results-preview.xml +448 -0
  57. data/test/data/results/5.0.2/results.xml +501 -0
  58. data/test/export_test_data.json +360 -0
  59. data/test/resultsreader_test_data.json +1119 -0
  60. data/test/services.server.info.xml +43 -0
  61. data/test/services.xml +111 -0
  62. data/test/test_atomfeed.rb +71 -0
  63. data/test/test_collection.rb +278 -0
  64. data/test/test_configuration_file.rb +124 -0
  65. data/test/test_context.rb +119 -0
  66. data/test/test_entity.rb +95 -0
  67. data/test/test_helper.rb +250 -0
  68. data/test/test_http_error.rb +52 -0
  69. data/test/test_index.rb +91 -0
  70. data/test/test_jobs.rb +319 -0
  71. data/test/test_messages.rb +17 -0
  72. data/test/test_namespace.rb +188 -0
  73. data/test/test_restarts.rb +49 -0
  74. data/test/test_resultsreader.rb +106 -0
  75. data/test/test_roles.rb +41 -0
  76. data/test/test_saved_searches.rb +119 -0
  77. data/test/test_service.rb +65 -0
  78. data/test/test_users.rb +33 -0
  79. data/test/test_xml_shim.rb +28 -0
  80. data/test/testfile.txt +1 -0
  81. 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