splunk-sdk-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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