redfish_client 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 6b0c14c35b85c191eac60fe2c468d59aee5495bd2fcc19c8fa70158eb10c934b
4
- data.tar.gz: e5b0d1bea6bc9c3912dc01724fade05f91a2bc90a9622cbbd0050aac66655e63
2
+ SHA1:
3
+ metadata.gz: 845f7a73083c73f8fe2ce79f8968332dd605aa87
4
+ data.tar.gz: 5e1e8056e6618c11428e0907050ef874242b78d3
5
5
  SHA512:
6
- metadata.gz: d8380f1294e5014e6b10cf9574381ed6a0a48d7ed6de9170020157311451902ad033527eaabe0ccc0f746dbfd1d603284f5293bfd52f318ad03685f6ca711c64
7
- data.tar.gz: 1f16c3bbe5132f62c317cc8d8afbada42f4263fc01bc895203fd9e9e3df0612bd4909d04366dcbb1626c2d2c4e9bb8dede4127483d4f17a930e0cd8331b1ea2b
6
+ metadata.gz: f2957a2b7baf789ef346b22e74e7cddb0894704eb30ae225ac4cd17cf95adf4e7f93d619859d2b3fb41c240bf2f34f1ffe09e2c3bc33110f11129d2664e19ca4
7
+ data.tar.gz: b3d582a79ce5a8bfe435d5d08d63977414c426a00b68cd1ac02555297c0000480a2dc6bdb1c6027e17585858304633f031d46e31a52f04f9f4d4ab2b346cc7b9
data/.rubocop.yml CHANGED
@@ -9,6 +9,9 @@ AllCops:
9
9
  Layout/MultilineOperationIndentation:
10
10
  EnforcedStyle: indented
11
11
 
12
+ Layout/MultilineMethodCallIndentation:
13
+ EnforcedStyle: indented
14
+
12
15
  Style/MethodMissing:
13
16
  Exclude:
14
17
  - lib/redfish_client/resource.rb
@@ -22,9 +25,24 @@ Style/Documentation:
22
25
  Style/BracesAroundHashParameters:
23
26
  EnforcedStyle: context_dependent
24
27
 
28
+ Style/TrailingCommaInArguments:
29
+ EnforcedStyleForMultiline: comma
30
+
31
+ Style/TrailingCommaInArrayLiteral:
32
+ EnforcedStyleForMultiline: comma
33
+
34
+ Style/TrailingCommaInHashLiteral:
35
+ EnforcedStyleForMultiline: comma
36
+
25
37
  Metrics/AbcSize:
26
38
  Max: 20
27
39
 
28
40
  Metrics/BlockLength:
29
41
  Exclude:
30
42
  - spec/**/*.rb
43
+
44
+ Metrics/ClassLength:
45
+ Max: 500
46
+
47
+ Metrics/ParameterLists:
48
+ CountKeywordArgs: false
data/.travis.yml CHANGED
@@ -1,4 +1,3 @@
1
- sudo: false
2
1
  language: ruby
3
2
  cache: bundler
4
3
 
data/README.md CHANGED
@@ -41,6 +41,46 @@ Minimal program that uses this gem would look something like this:
41
41
  root.logout
42
42
 
43
43
 
44
+ ## Handling asynchronous operations
45
+
46
+ Redfish service can return a 202 status when we request an execution of a
47
+ long-running operation (e.g. updating firmware). We are expected to poll the
48
+ monitor for changes until the job terminates.
49
+
50
+ Responses in Redfish client have a built-in support for this, so polling the
51
+ service is rather painless:
52
+
53
+ # Start the async action
54
+ response = update_service.Actions["#UpdateService.SimpleUpdate"].post(
55
+ field: "target", payload: { ... },
56
+ )
57
+ # Wait for the termination
58
+ response = update_service.wait(response)
59
+ # Do something with response
60
+
61
+ It is also possible to manually poll the response like this:
62
+
63
+ response = update_service.Actions["#UpdateService.SimpleUpdate"].post(
64
+ field: "target", payload: { ... },
65
+ )
66
+ until response.done?
67
+ # wait a bit
68
+ response = update_service.get(response.monitor)
69
+ end
70
+
71
+ Response is also safe to (de)serialize, which means that the process that
72
+ started the async operation and the process that will wait for it can be
73
+ separate:
74
+
75
+ response = update_service.Actions["#UpdateService.SimpleUpdate"].post(
76
+ field: "target", payload: { ... },
77
+ )
78
+ send_response_somewhere(response.to_h)
79
+
80
+ # Somewhere else
81
+ response = Response.from_hash(receive_response_from_somewhere)
82
+
83
+
44
84
  ## Development
45
85
 
46
86
  After checking out the repo, run `bin/setup` to install dependencies. Then,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redfish_client/caching_connector"
4
3
  require "redfish_client/connector"
4
+ require "redfish_client/nil_hash"
5
5
  require "redfish_client/root"
6
6
  require "redfish_client/version"
7
7
 
@@ -13,11 +13,8 @@ module RedfishClient
13
13
  # @param verify [Boolean] verify certificates for https connections
14
14
  # @param use_cache [Boolean] cache API responses
15
15
  def self.new(url, prefix: "/redfish/v1", verify: true, use_cache: true)
16
- con = if use_cache
17
- CachingConnector.new(url, verify)
18
- else
19
- Connector.new(url, verify)
20
- end
16
+ cache = (use_cache ? Hash : NilHash).new
17
+ con = Connector.new(url, verify: verify, cache: cache)
21
18
  Root.new(con, oid: prefix)
22
19
  end
23
20
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
3
4
  require "excon"
4
5
  require "json"
5
6
 
7
+ require "redfish_client/nil_hash"
8
+ require "redfish_client/response"
9
+
6
10
  module RedfishClient
7
11
  # Connector serves as a low-level wrapper around HTTP calls that are used
8
12
  # to retrieve data from the service API. It abstracts away implementation
@@ -12,18 +16,38 @@ module RedfishClient
12
16
  # Library users should treat this class as an implementation detail and
13
17
  # use higer-level {RedfishClient::Resource} instead.
14
18
  class Connector
19
+ # AuthError is raised if the credentials are invalid.
20
+ class AuthError < StandardError; end
21
+
15
22
  # Default headers, as required by Redfish spec
16
23
  # https://redfish.dmtf.org/schemas/DSP0266_1.4.0.html#request-headers
17
24
  DEFAULT_HEADERS = {
18
25
  "Accept" => "application/json",
19
- "OData-Version" => "4.0"
26
+ "OData-Version" => "4.0",
20
27
  }.freeze
21
28
 
29
+ # Basic and token authentication header names
30
+ BASIC_AUTH_HEADER = "Authorization"
31
+ TOKEN_AUTH_HEADER = "X-Auth-Token"
32
+
22
33
  # Create new connector.
23
34
  #
35
+ # By default, connector performs no caching. If caching is desired,
36
+ # Hash should be used as a cache implementation.
37
+ #
38
+ # It is also possible to pass in custom caching class. Instances of that
39
+ # class should respond to the following four methods:
40
+ #
41
+ # 1. `[](key)` - Used to access cached content and should return
42
+ # `nil` if the key has no associated value.
43
+ # 2. `[]=(key, value)` - Cache `value` under the `key`
44
+ # 3. `clear` - Clear the complete cache.
45
+ # 4. `delete(key)` - Invalidate cache entry associated with `key`.
46
+ #
24
47
  # @param url [String] base url of the Redfish service
25
48
  # @param verify [Boolean] verify SSL certificate of the service
26
- def initialize(url, verify = true)
49
+ # @param cache [Object] cache backend
50
+ def initialize(url, verify: true, cache: nil)
27
51
  @url = url
28
52
  @headers = DEFAULT_HEADERS.dup
29
53
  middlewares = Excon.defaults[:middlewares] +
@@ -31,6 +55,7 @@ module RedfishClient
31
55
  @connection = Excon.new(@url,
32
56
  ssl_verify_peer: verify,
33
57
  middlewares: middlewares)
58
+ @cache = cache || NilHash.new
34
59
  end
35
60
 
36
61
  # Add HTTP headers to the requests made by the connector.
@@ -50,44 +75,125 @@ module RedfishClient
50
75
  headers.each { |h| @headers.delete(h) }
51
76
  end
52
77
 
78
+ # Issue requests to the service.
79
+ #
80
+ # @param mathod [Symbol] HTTP method (:get, :post, :patch or :delete)
81
+ # @param path [String] path to the resource, relative to the base
82
+ # @param data [Hash] data to be sent over the socket
83
+ # @return [Response] response object
84
+ def request(method, path, data = nil)
85
+ params = prepare_request_params(method, path, data)
86
+ r = @connection.request(params)
87
+ if r.status == 401
88
+ login
89
+ r = @connection.request(params)
90
+ end
91
+ Response.new(r.status, downcase_headers(r.data[:headers]), r.data[:body])
92
+ end
93
+
53
94
  # Issue GET request to service.
54
95
  #
96
+ # This method will first try to return cached response if available. If
97
+ # cache does not contain entry for this request, data will be fetched from
98
+ # remote and then cached, but only if the response has an OK (200) status.
99
+ #
55
100
  # @param path [String] path to the resource, relative to the base url
56
- # @return [Excon::Response] response object
101
+ # @return [Response] response object
57
102
  def get(path)
58
- @connection.get(path: path, headers: @headers)
103
+ return @cache[path] if @cache[path]
104
+
105
+ request(:get, path).tap { |r| @cache[path] = r if r.status == 200 }
59
106
  end
60
107
 
61
108
  # Issue POST requests to the service.
62
109
  #
63
110
  # @param path [String] path to the resource, relative to the base
64
111
  # @param data [Hash] data to be sent over the socket, JSON encoded
65
- # @return [Excon::Response] response object
112
+ # @return [Response] response object
66
113
  def post(path, data = nil)
67
- @connection.post(prepare_request_params(path, data))
114
+ request(:post, path, data)
68
115
  end
69
116
 
70
117
  # Issue PATCH requests to the service.
71
118
  #
72
119
  # @param path [String] path to the resource, relative to the base
73
120
  # @param data [Hash] data to be sent over the socket
74
- # @return [Excon::Response] response object
121
+ # @return [Response] response object
75
122
  def patch(path, data = nil)
76
- @connection.patch(prepare_request_params(path, data))
123
+ request(:patch, path, data)
77
124
  end
78
125
 
79
126
  # Issue DELETE requests to the service.
80
127
  #
81
128
  # @param path [String] path to the resource, relative to the base
82
- # @return [Excon::Response] response object
129
+ # @return [Response] response object
83
130
  def delete(path)
84
- @connection.delete(path: path, headers: @headers)
131
+ request(:delete, path)
132
+ end
133
+
134
+ # Clear the cached responses.
135
+ #
136
+ # If path is passed as a parameter, only one cache entry gets invalidated,
137
+ # else complete cache gets invalidated.
138
+ #
139
+ # Next GET request will repopulate the cache.
140
+ #
141
+ # @param path [String] path to invalidate
142
+ def reset(path = nil)
143
+ path.nil? ? @cache.clear : @cache.delete(path)
144
+ end
145
+
146
+ # Set authentication-related variables.
147
+ #
148
+ # Last parameter controls the kind of login connector will perform. If
149
+ # session_path is `nil`, basic authentication will be used, otherwise
150
+ # connector will use session-based authentication.
151
+ #
152
+ # Note that actual login is done lazily. If you need to check for
153
+ # credential validity, call #{login} method.
154
+ #
155
+ # @param username [String] API username
156
+ # @param password [String] API password
157
+ # @param auth_test_path [String] API path to test credential's validity
158
+ # @param session_path [String, nil] API session path
159
+ def set_auth_info(username, password, auth_test_path, session_path = nil)
160
+ @username = username
161
+ @password = password
162
+ @auth_test_path = auth_test_path
163
+ @session_path = session_path
164
+ end
165
+
166
+ # Authenticate against the service.
167
+ #
168
+ # Calling this method will try to authenticate against API using
169
+ # credentials provided by #{set_auth_info} call.
170
+ # If authentication fails, # {AuthError} will be raised.
171
+ #
172
+ # @raise [AuthError] if credentials are invalid
173
+ def login
174
+ @session_path ? session_login : basic_login
175
+ end
176
+
177
+ # Sign out of the service.
178
+ def logout
179
+ # We bypass request here because we do not want any retries on 401
180
+ # when doing logout.
181
+ if @session_oid
182
+ params = prepare_request_params(:delete, @session_oid)
183
+ @connection.request(params)
184
+ @session_oid = nil
185
+ end
186
+ remove_headers([BASIC_AUTH_HEADER, TOKEN_AUTH_HEADER])
85
187
  end
86
188
 
87
189
  private
88
190
 
89
- def prepare_request_params(path, data)
90
- params = { path: path }
191
+ def downcase_headers(headers)
192
+ headers.each_with_object({}) { |(k, v), obj| obj[k.downcase] = v }
193
+ end
194
+
195
+ def prepare_request_params(method, path, data = nil)
196
+ params = { method: method, path: path }
91
197
  if data
92
198
  params[:body] = data.to_json
93
199
  params[:headers] = @headers.merge("Content-Type" => "application/json")
@@ -96,5 +202,40 @@ module RedfishClient
96
202
  end
97
203
  params
98
204
  end
205
+
206
+ def session_login
207
+ # We bypass request here because we do not want any retries on 401
208
+ # when doing login.
209
+ params = prepare_request_params(:post, @session_path,
210
+ "UserName" => @username,
211
+ "Password" => @password)
212
+ r = @connection.request(params)
213
+ raise_invalid_auth_error unless r.status == 201
214
+
215
+ token = r.data[:headers][TOKEN_AUTH_HEADER]
216
+ add_headers(TOKEN_AUTH_HEADER => token)
217
+ @session_oid = JSON.parse(r.data[:body])["@odata.id"]
218
+ end
219
+
220
+ def basic_login
221
+ payload = Base64.encode64("#{@username}:#{@password}").strip
222
+ add_headers(BASIC_AUTH_HEADER => "Basic #{payload}")
223
+ return if auth_valid?
224
+
225
+ remove_headers([BASIC_AUTH_HEADER])
226
+ raise_invalid_auth_error
227
+ end
228
+
229
+ def raise_invalid_auth_error
230
+ raise AuthError, "Invalid credentials"
231
+ end
232
+
233
+ def auth_valid?
234
+ # We bypass request here because we do not want any retries on 401
235
+ # when checking authentication headers.
236
+ reset(@auth_test_path) # Do not want to see cached response
237
+ params = prepare_request_params(:get, @auth_test_path)
238
+ @connection.request(params).status == 200
239
+ end
99
240
  end
100
241
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedfishClient
4
+ # NilHash imitates the built-in Hash class without storing anything
5
+ # permanently.
6
+ #
7
+ # Main use of this class is as a non-caching connector backend.
8
+ class NilHash
9
+ # Access hash member.
10
+ #
11
+ # Since this implementation does not store any data, return value is
12
+ # always nil.
13
+ #
14
+ # @param _key not used
15
+ # @return [nil]
16
+ def [](_key)
17
+ nil
18
+ end
19
+
20
+ # Set hash member.
21
+ #
22
+ # This is just a pass-through method, since it always simply returns the
23
+ # value without actually storing it.
24
+ #
25
+ # @param _key not used
26
+ # @param value [Object] any value
27
+ # @return [Object] value
28
+ def []=(_key, value)
29
+ value
30
+ end
31
+
32
+ # Clear the contents of the cache.
33
+ #
34
+ # Since hash is not storing anything, this is a no-op.
35
+ def clear; end
36
+
37
+ # Delete entry from hash.
38
+ #
39
+ # Since hash is not storing anything, this is a no-op.
40
+ #
41
+ # @param _key not used
42
+ def delete(_key) end
43
+ end
44
+ end
@@ -17,8 +17,8 @@ module RedfishClient
17
17
  #
18
18
  # In order to reduce the amount of requests being sent to the service,
19
19
  # resource can also utilise caching connector. If we would like to get
20
- # fresh values from the service, {#reset} call will flush the cache,
21
- # causing next access to retrieve fresh data.
20
+ # fresh values from the service, {#refresh} call will flush the cache and
21
+ # retrieve fresh data from the remote.
22
22
  class Resource
23
23
  # NoODataId error is raised when operation would need OpenData id of the
24
24
  # resource to accomplish the task a hand.
@@ -28,9 +28,20 @@ module RedfishClient
28
28
  # resource.
29
29
  class NoResource < StandardError; end
30
30
 
31
+ # Timeout error is raised if the async request is not handled in due time.
32
+ class Timeout < StandardError; end
33
+
31
34
  # Headers, returned from the service when resource has been constructed.
35
+ #
36
+ # @return [Hash] resource headers
32
37
  attr_reader :headers
33
38
 
39
+ # Raw data that has been used to construct resource by either fetching it
40
+ # from the remote API or by being passed-in as a parameter to constructor.
41
+ #
42
+ # @return [Hash] resource raw data
43
+ attr_reader :raw
44
+
34
45
  # Create new resource.
35
46
  #
36
47
  # Resource can be created either by passing in OpenData identifier or
@@ -41,16 +52,36 @@ module RedfishClient
41
52
  # @param connector [RedfishClient::Connector] connector that will be used
42
53
  # to fetch the resources
43
54
  # @param oid [String] OpenData id of the resource
44
- # @param content [Hash] content to populate resource with
55
+ # @param raw [Hash] raw content to populate resource with
45
56
  # @raise [NoResource] resource cannot be retrieved from the service
46
- def initialize(connector, oid: nil, content: nil)
57
+ def initialize(connector, oid: nil, raw: nil)
47
58
  @connector = connector
48
-
49
59
  if oid
50
60
  initialize_from_service(oid)
51
61
  else
52
- @content = content
62
+ @raw = raw
63
+ end
64
+ end
65
+
66
+ # Wait for the potentially async operation to terminate
67
+ #
68
+ # Note that this can be safely called on response from non-async
69
+ # operations where the function will return immediately and without making
70
+ # any additional requests to the service.
71
+ #
72
+ # @param response [RedfishClient::Response] response
73
+ # @param retries [Integer] number of retries
74
+ # @param delay [Integer] number of seconds between retries
75
+ # @return [RedfishClient::Response] final response
76
+ # @raise [Timeout] if the operation did not terminate in time
77
+ def wait(response, retries: 10, delay: 1)
78
+ retries.times do |_i|
79
+ return response if response.done?
80
+
81
+ sleep(delay)
82
+ response = get(path: response.monitor)
53
83
  end
84
+ raise Timeout, "Async operation did not terminate in allotted time"
54
85
  end
55
86
 
56
87
  # Access resource content.
@@ -61,7 +92,7 @@ module RedfishClient
61
92
  # @param attr [String] key for accessing data
62
93
  # @return associated value or `nil` if attr is missing
63
94
  def [](attr)
64
- build_resource(@content[attr])
95
+ build_resource(raw[attr])
65
96
  end
66
97
 
67
98
  # Safely access nested resource content.
@@ -82,7 +113,7 @@ module RedfishClient
82
113
  # @param name [String, Symbol] key name to test
83
114
  # @return [Boolean] inclusion test result
84
115
  def key?(name)
85
- @content.key?(name.to_s)
116
+ raw.key?(name.to_s)
86
117
  end
87
118
 
88
119
  # Convenience access for resource data.
@@ -96,25 +127,52 @@ module RedfishClient
96
127
  key?(symbol.to_s) || super
97
128
  end
98
129
 
99
- # Clear the cached sub-resources.
130
+ # Pretty-print the wrapped content.
100
131
  #
101
- # This method is a no-op if connector in use does not support caching.
102
- def reset
103
- @connector.reset if @connector.respond_to?(:reset)
132
+ # @return [String] JSON-serialized raw data
133
+ def to_s
134
+ JSON.pretty_generate(raw)
104
135
  end
105
136
 
106
- # Access raw JSON data that resource wraps.
137
+ # Issue a requests to the selected endpoint.
138
+ #
139
+ # By default, request will be sent to the path, stored in `@odata.id`
140
+ # field. Source field can be changed by specifying the `field` parameter
141
+ # when calling this function. Specifying the `path` argument will bypass
142
+ # the field lookup altogether and issue a request directly to the selected
143
+ # path.
107
144
  #
108
- # @return [Hash] wrapped data
109
- def raw
110
- @content
145
+ # If the resource has no lookup field, {NoODataId} error will be raised,
146
+ # since posting to non-networked resources makes no sense and probably
147
+ # indicates bug in library consumer.
148
+ #
149
+ # @param method [Symbol] HTTP method (:get, :post, :patch or :delete)
150
+ # @param field [String, Symbol] path lookup field
151
+ # @param path [String] path to post to
152
+ # @return [RedfishClient::Response] response
153
+ # @raise [NoODataId] resource has no OpenData id
154
+ def request(method, field, path, payload = nil)
155
+ @connector.request(method, get_path(field, path), payload)
111
156
  end
112
157
 
113
- # Pretty-print the wrapped content.
158
+ # Issue a GET requests to the selected endpoint.
114
159
  #
115
- # @return [String] JSON-serialized raw data
116
- def to_s
117
- JSON.pretty_generate(@content)
160
+ # By default, GET request will be sent to the path, stored in `@odata.id`
161
+ # field. Source field can be changed by specifying the `field` parameter
162
+ # when calling this function. Specifying the `path` argument will bypass
163
+ # the field lookup altogether and issue a GET request directly to the
164
+ # selected path.
165
+ #
166
+ # If the resource has no lookup field, {NoODataId} error will be raised,
167
+ # since posting to non-networked resources makes no sense and probably
168
+ # indicates bug in library consumer.
169
+ #
170
+ # @param field [String, Symbol] path lookup field
171
+ # @param path [String] path to post to
172
+ # @return [RedfishClient::Response] response
173
+ # @raise [NoODataId] resource has no OpenData id
174
+ def get(field: "@odata.id", path: nil)
175
+ request(:get, field, path)
118
176
  end
119
177
 
120
178
  # Issue a POST requests to the selected endpoint.
@@ -135,10 +193,10 @@ module RedfishClient
135
193
  # @param field [String, Symbol] path lookup field
136
194
  # @param path [String] path to post to
137
195
  # @param payload [Hash<String, >] data to send
138
- # @return [Excon::Response] response
196
+ # @return [RedfishClient::Response] response
139
197
  # @raise [NoODataId] resource has no OpenData id
140
198
  def post(field: "@odata.id", path: nil, payload: nil)
141
- @connector.post(get_path(field, path), payload)
199
+ request(:post, field, path, payload)
142
200
  end
143
201
 
144
202
  # Issue a PATCH requests to the selected endpoint.
@@ -149,10 +207,10 @@ module RedfishClient
149
207
  # @param field [String, Symbol] path lookup field
150
208
  # @param path [String] path to patch
151
209
  # @param payload [Hash<String, >] data to send
152
- # @return [Excon::Response] response
210
+ # @return [RedfishClient::Response] response
153
211
  # @raise [NoODataId] resource has no OpenData id
154
212
  def patch(field: "@odata.id", path: nil, payload: nil)
155
- @connector.patch(get_path(field, path), payload)
213
+ request(:patch, field, path, payload)
156
214
  end
157
215
 
158
216
  # Issue a DELETE requests to the endpoint of the resource.
@@ -161,26 +219,52 @@ module RedfishClient
161
219
  # raised, since deleting non-networked resources makes no sense and
162
220
  # probably indicates bug in library consumer.
163
221
  #
164
- # @return [Excon::Response] response
222
+ # @return [RedfishClient::Response] response
165
223
  # @raise [NoODataId] resource has no OpenData id
166
- def delete
167
- @connector.delete(get_path("@odata.id", nil))
224
+ def delete(field: "@odata.id", path: nil, payload: nil)
225
+ request(:delete, field, path, payload)
226
+ end
227
+
228
+ # Refresh resource content from the API
229
+ #
230
+ # Caling this method will ensure that the resource data is in sync with
231
+ # the Redfis API, invalidating any caches as necessary.
232
+ def refresh
233
+ return unless self["@odata.id"]
234
+
235
+ # TODO(@tadeboro): raise more sensible exception if resource cannot be
236
+ # refreshed.
237
+ @connector.reset(self["@odata.id"])
238
+ initialize_from_service(self["@odata.id"])
168
239
  end
169
240
 
170
241
  private
171
242
 
172
243
  def initialize_from_service(oid)
173
- resp = @connector.get(oid)
174
- raise NoResource unless resp.status == 200
244
+ url, fragment = oid.split("#", 2)
245
+ resp = wait(get(path: url))
246
+ raise NoResource unless [200, 201].include?(resp.status)
247
+
248
+ @raw = get_fragment(JSON.parse(resp.body), fragment)
249
+ @raw["@odata.id"] = oid
250
+ @headers = resp.headers
251
+ end
252
+
253
+ def get_fragment(data, fragment)
254
+ # data, /my/0/part -> data["my"][0]["part"]
255
+ parse_fragment_string(fragment).reduce(data) do |acc, c|
256
+ acc[acc.is_a?(Array) ? c.to_i : c]
257
+ end
258
+ end
175
259
 
176
- @content = JSON.parse(resp.data[:body])
177
- @content["@odata.id"] = oid
178
- @headers = resp.data[:headers]
260
+ def parse_fragment_string(fragment)
261
+ # /my/0/part -> ["my", "0", "part"]
262
+ fragment ? fragment.split("/").reject { |i| i == "" } : []
179
263
  end
180
264
 
181
265
  def get_path(field, path)
182
266
  raise NoODataId if path.nil? && !key?(field)
183
- path || @content[field]
267
+ path || raw[field]
184
268
  end
185
269
 
186
270
  def build_resource(data)
@@ -197,7 +281,7 @@ module RedfishClient
197
281
  if data.key?("@odata.id")
198
282
  Resource.new(@connector, oid: data["@odata.id"])
199
283
  else
200
- Resource.new(@connector, content: data)
284
+ Resource.new(@connector, raw: data)
201
285
  end
202
286
  rescue NoResource
203
287
  nil
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedfishClient
4
+ # Response struct.
5
+ #
6
+ # This struct is returned from the methods that interact with the remote API.
7
+ class Response
8
+ attr_reader :status
9
+ attr_reader :headers
10
+ attr_reader :body
11
+
12
+ def initialize(status, headers, body)
13
+ @status = status
14
+ @headers = headers
15
+ @body = body
16
+ end
17
+
18
+ def done?
19
+ status != 202
20
+ end
21
+
22
+ def monitor
23
+ done? ? nil : headers["location"]
24
+ end
25
+
26
+ def to_h
27
+ { "status" => status, "headers" => headers, "body" => body }
28
+ end
29
+
30
+ def self.from_hash(data)
31
+ new(*data.values_at("status", "headers", "body"))
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
- require "json"
5
3
  require "server_sent_events"
6
4
 
7
5
  require "redfish_client/event_listener"
@@ -11,41 +9,6 @@ module RedfishClient
11
9
  # Root resource represents toplevel entry point into Redfish service data.
12
10
  # Its main purpose is to provide authentication support for the API.
13
11
  class Root < Resource
14
- # AuthError is raised if the user session cannot be created.
15
- class AuthError < StandardError; end
16
-
17
- # Basic and token authentication headers.
18
- BASIC_AUTH_HEADER = "Authorization"
19
- TOKEN_AUTH_HEADER = "X-Auth-Token"
20
-
21
- # Authenticate against the service.
22
- #
23
- # Calling this method will try to create new session on the service using
24
- # provided credentials. If the session creation fails, basic
25
- # authentication will be attempted. If basic authentication fails,
26
- # {AuthError} will be raised.
27
- #
28
- # @param username [String] username
29
- # @param password [String] password
30
- # @raise [AuthError] if user session could not be created
31
- def login(username, password)
32
- # Since session auth is more secure, we try it first and use basic auth
33
- # only if session auth is not available.
34
- if session_login_available?
35
- session_login(username, password)
36
- else
37
- basic_login(username, password)
38
- end
39
- end
40
-
41
- # Sign out of the service.
42
- #
43
- # If the session could not be deleted, {AuthError} will be raised.
44
- def logout
45
- session_logout
46
- basic_logout
47
- end
48
-
49
12
  # Find Redfish service object by OData ID field.
50
13
  #
51
14
  # @param oid [String] Odata id of the resource
@@ -77,47 +40,38 @@ module RedfishClient
77
40
  EventListener.new(ServerSentEvents.create_client(address))
78
41
  end
79
42
 
80
- private
81
-
82
- def session_login_available?
83
- !@content.dig("Links", "Sessions").nil?
84
- end
85
-
86
- def session_login(username, password)
87
- r = @connector.post(
88
- @content["Links"]["Sessions"]["@odata.id"],
89
- "UserName" => username, "Password" => password
43
+ # Authenticate against the service.
44
+ #
45
+ # Calling this method will select the appropriate method of authentication
46
+ # and try to login using provided credentials.
47
+ #
48
+ # @param username [String] username
49
+ # @param password [String] password
50
+ # @raise [RedfishClient::AuthenticatedConnector::AuthError] if user
51
+ # session could not be authenticated
52
+ def login(username, password)
53
+ @connector.set_auth_info(
54
+ username, password, auth_test_path, session_path
90
55
  )
91
- raise AuthError, "Invalid credentials" unless r.status == 201
92
-
93
- session_logout
94
-
95
- payload = r.data[:headers][TOKEN_AUTH_HEADER]
96
- @connector.add_headers(TOKEN_AUTH_HEADER => payload)
97
- @session = Resource.new(@connector, content: JSON.parse(r.data[:body]))
56
+ @connector.login
98
57
  end
99
58
 
100
- def session_logout
101
- return unless @session
102
- r = @session.delete
103
- raise AuthError unless r.status == 204
104
- @session = nil
105
- @connector.remove_headers([TOKEN_AUTH_HEADER])
59
+ # Sign out of the service.
60
+ def logout
61
+ @connector.logout
106
62
  end
107
63
 
108
- def auth_test_path
109
- @content.values.map { |v| v["@odata.id"] }.compact.first
110
- end
64
+ private
111
65
 
112
- def basic_login(username, password)
113
- payload = Base64.encode64("#{username}:#{password}").strip
114
- @connector.add_headers(BASIC_AUTH_HEADER => "Basic #{payload}")
115
- r = @connector.get(auth_test_path)
116
- raise AuthError, "Invalid credentials" unless r.status == 200
66
+ def session_path
67
+ # We access raw values here on purpose, since calling dig on resource
68
+ # instance would try to download the sessions collection, which would
69
+ # fail since we are not yet logged in.
70
+ raw.dig("Links", "Sessions", "@odata.id")
117
71
  end
118
72
 
119
- def basic_logout
120
- @connector.remove_headers([BASIC_AUTH_HEADER])
73
+ def auth_test_path
74
+ raw.values.find { |v| v["@odata.id"] }["@odata.id"]
121
75
  end
122
76
  end
123
77
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RedfishClient
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -32,10 +32,10 @@ Gem::Specification.new do |spec|
32
32
  spec.add_runtime_dependency "excon", "~> 0.60"
33
33
  spec.add_runtime_dependency "server_sent_events", "~> 0.1"
34
34
 
35
- spec.add_development_dependency "bundler", "~> 1.16"
36
35
  spec.add_development_dependency "rake", ">= 11.0"
37
36
  spec.add_development_dependency "rspec", ">= 3.7"
38
37
  spec.add_development_dependency "simplecov"
38
+ spec.add_development_dependency "webmock", "~> 3.4"
39
39
  spec.add_development_dependency "yard"
40
40
  spec.add_development_dependency "rubocop", "~> 0.54.0"
41
41
  spec.add_development_dependency "pry"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redfish_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tadej Borovšak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-18 00:00:00.000000000 Z
11
+ date: 2019-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.1'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '1.16'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '1.16'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +80,20 @@ dependencies:
94
80
  - - ">="
95
81
  - !ruby/object:Gem::Version
96
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.4'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: yard
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -156,10 +156,11 @@ files:
156
156
  - bin/console
157
157
  - bin/setup
158
158
  - lib/redfish_client.rb
159
- - lib/redfish_client/caching_connector.rb
160
159
  - lib/redfish_client/connector.rb
161
160
  - lib/redfish_client/event_listener.rb
161
+ - lib/redfish_client/nil_hash.rb
162
162
  - lib/redfish_client/resource.rb
163
+ - lib/redfish_client/response.rb
163
164
  - lib/redfish_client/root.rb
164
165
  - lib/redfish_client/version.rb
165
166
  - redfish_client.gemspec
@@ -184,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
185
  version: '0'
185
186
  requirements: []
186
187
  rubyforge_project:
187
- rubygems_version: 2.7.7
188
+ rubygems_version: 2.6.14
188
189
  signing_key:
189
190
  specification_version: 4
190
191
  summary: Simple Redfish client library
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redfish_client/connector"
4
-
5
- module RedfishClient
6
- # Variant of {RedfishClient::Connector} that caches GET responses.
7
- class CachingConnector < Connector
8
- # Create new caching connector.
9
- #
10
- # @param url [String] base url of the Redfish service
11
- # @param verify [Boolean] verify SSL certificate of the service
12
- def initialize(url, verify = true)
13
- super
14
- @cache = {}
15
- end
16
-
17
- # Issue GET request to service.
18
- #
19
- # Request is only issued if there is no cache entry for the existing path.
20
- #
21
- # @param path [String] path to the resource, relative to the base url
22
- # @return [Excon::Response] response object
23
- def get(path)
24
- @cache[path] ||= super
25
- end
26
-
27
- # Clear the cached responses.
28
- #
29
- # Next GET request will repopulate the cache.
30
- def reset(path: nil)
31
- if path.nil?
32
- @cache = {}
33
- else
34
- @cache.delete(path)
35
- end
36
- end
37
- end
38
- end