redfish_client 0.6.0 → 0.6.1
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.
- checksums.yaml +4 -4
- metadata +12 -28
- data/.codeclimate.yml +0 -4
- data/.github/workflows/ci.yml +0 -21
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.rubocop.yml +0 -48
- data/.simplecov +0 -3
- data/.yardopts +0 -3
- data/Gemfile +0 -6
- data/LICENSE +0 -202
- data/README.md +0 -98
- data/Rakefile +0 -6
- data/bin/console +0 -10
- data/bin/setup +0 -8
- data/lib/redfish_client/connector.rb +0 -262
- data/lib/redfish_client/event_listener.rb +0 -35
- data/lib/redfish_client/nil_hash.rb +0 -44
- data/lib/redfish_client/resource.rb +0 -290
- data/lib/redfish_client/response.rb +0 -47
- data/lib/redfish_client/root.rb +0 -77
- data/lib/redfish_client/version.rb +0 -5
- data/lib/redfish_client.rb +0 -21
- data/redfish_client.gemspec +0 -42
@@ -1,262 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "base64"
|
4
|
-
require "excon"
|
5
|
-
require "json"
|
6
|
-
|
7
|
-
require "redfish_client/nil_hash"
|
8
|
-
require "redfish_client/response"
|
9
|
-
|
10
|
-
module RedfishClient
|
11
|
-
# Connector serves as a low-level wrapper around HTTP calls that are used
|
12
|
-
# to retrieve data from the service API. It abstracts away implementation
|
13
|
-
# details such as sending the proper headers in request, which do not
|
14
|
-
# change between resource fetches.
|
15
|
-
#
|
16
|
-
# Library users should treat this class as an implementation detail and
|
17
|
-
# use higer-level {RedfishClient::Resource} instead.
|
18
|
-
class Connector
|
19
|
-
# AuthError is raised if the credentials are invalid.
|
20
|
-
class AuthError < StandardError; end
|
21
|
-
|
22
|
-
# Default headers, as required by Redfish spec
|
23
|
-
# https://redfish.dmtf.org/schemas/DSP0266_1.4.0.html#request-headers
|
24
|
-
DEFAULT_HEADERS = {
|
25
|
-
"Accept" => "application/json",
|
26
|
-
"OData-Version" => "4.0",
|
27
|
-
}.freeze
|
28
|
-
|
29
|
-
# Basic and token authentication header names
|
30
|
-
BASIC_AUTH_HEADER = "Authorization"
|
31
|
-
TOKEN_AUTH_HEADER = "X-Auth-Token"
|
32
|
-
LOCATION_HEADER = "Location"
|
33
|
-
|
34
|
-
# Create new connector.
|
35
|
-
#
|
36
|
-
# By default, connector performs no caching. If caching is desired,
|
37
|
-
# Hash should be used as a cache implementation.
|
38
|
-
#
|
39
|
-
# It is also possible to pass in custom caching class. Instances of that
|
40
|
-
# class should respond to the following four methods:
|
41
|
-
#
|
42
|
-
# 1. `[](key)` - Used to access cached content and should return
|
43
|
-
# `nil` if the key has no associated value.
|
44
|
-
# 2. `[]=(key, value)` - Cache `value` under the `key`
|
45
|
-
# 3. `clear` - Clear the complete cache.
|
46
|
-
# 4. `delete(key)` - Invalidate cache entry associated with `key`.
|
47
|
-
#
|
48
|
-
# @param url [String] base url of the Redfish service
|
49
|
-
# @param verify [Boolean] verify SSL certificate of the service
|
50
|
-
# @param use_session [Boolean] Use a session for authentication
|
51
|
-
# @param cache [Object] cache backend
|
52
|
-
def initialize(url, verify: true, cache: nil, use_session: true)
|
53
|
-
@url = url
|
54
|
-
@headers = DEFAULT_HEADERS.dup
|
55
|
-
middlewares = Excon.defaults[:middlewares] +
|
56
|
-
[Excon::Middleware::RedirectFollower]
|
57
|
-
@connection = Excon.new(@url,
|
58
|
-
ssl_verify_peer: verify,
|
59
|
-
middlewares: middlewares)
|
60
|
-
@cache = cache || NilHash.new
|
61
|
-
@use_session = use_session
|
62
|
-
end
|
63
|
-
|
64
|
-
# Add HTTP headers to the requests made by the connector.
|
65
|
-
#
|
66
|
-
# @param headers [Hash<String, String>] headers to be added
|
67
|
-
def add_headers(headers)
|
68
|
-
@headers.merge!(headers)
|
69
|
-
end
|
70
|
-
|
71
|
-
# Remove HTTP headers from requests made by the connector.
|
72
|
-
#
|
73
|
-
# Headers that are not currently set are silently ignored and no error is
|
74
|
-
# raised.
|
75
|
-
#
|
76
|
-
# @param headers [List<String>] headers to remove
|
77
|
-
def remove_headers(headers)
|
78
|
-
headers.each { |h| @headers.delete(h) }
|
79
|
-
end
|
80
|
-
|
81
|
-
# Issue requests to the service.
|
82
|
-
#
|
83
|
-
# @param mathod [Symbol] HTTP method (:get, :post, :patch or :delete)
|
84
|
-
# @param path [String] path to the resource, relative to the base
|
85
|
-
# @param data [Hash] data to be sent over the socket
|
86
|
-
# @return [Response] response object
|
87
|
-
def request(method, path, data = nil)
|
88
|
-
return @cache[path] if method == :get && @cache[path]
|
89
|
-
|
90
|
-
do_request(method, path, data).tap do |r|
|
91
|
-
@cache[path] = r if method == :get && r.status == 200
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# Issue GET request to service.
|
96
|
-
#
|
97
|
-
# This method will first try to return cached response if available. If
|
98
|
-
# cache does not contain entry for this request, data will be fetched from
|
99
|
-
# remote and then cached, but only if the response has an OK (200) status.
|
100
|
-
#
|
101
|
-
# @param path [String] path to the resource, relative to the base url
|
102
|
-
# @return [Response] response object
|
103
|
-
def get(path)
|
104
|
-
request(:get, path)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Issue POST requests to the service.
|
108
|
-
#
|
109
|
-
# @param path [String] path to the resource, relative to the base
|
110
|
-
# @param data [Hash] data to be sent over the socket, JSON encoded
|
111
|
-
# @return [Response] response object
|
112
|
-
def post(path, data = nil)
|
113
|
-
request(:post, path, data)
|
114
|
-
end
|
115
|
-
|
116
|
-
# Issue PATCH requests to the service.
|
117
|
-
#
|
118
|
-
# @param path [String] path to the resource, relative to the base
|
119
|
-
# @param data [Hash] data to be sent over the socket
|
120
|
-
# @return [Response] response object
|
121
|
-
def patch(path, data = nil)
|
122
|
-
request(:patch, path, data)
|
123
|
-
end
|
124
|
-
|
125
|
-
# Issue DELETE requests to the service.
|
126
|
-
#
|
127
|
-
# @param path [String] path to the resource, relative to the base
|
128
|
-
# @return [Response] response object
|
129
|
-
def delete(path)
|
130
|
-
request(:delete, path)
|
131
|
-
end
|
132
|
-
|
133
|
-
# Clear the cached responses.
|
134
|
-
#
|
135
|
-
# If path is passed as a parameter, only one cache entry gets invalidated,
|
136
|
-
# else complete cache gets invalidated.
|
137
|
-
#
|
138
|
-
# Next GET request will repopulate the cache.
|
139
|
-
#
|
140
|
-
# @param path [String] path to invalidate
|
141
|
-
def reset(path = nil)
|
142
|
-
path.nil? ? @cache.clear : @cache.delete(path)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Set authentication-related variables.
|
146
|
-
#
|
147
|
-
# Last parameter controls the kind of login connector will perform. If
|
148
|
-
# session_path is `nil`, basic authentication will be used, otherwise
|
149
|
-
# connector will use session-based authentication.
|
150
|
-
#
|
151
|
-
# Note that actual login is done lazily. If you need to check for
|
152
|
-
# credential validity, call #{login} method.
|
153
|
-
#
|
154
|
-
# @param username [String] API username
|
155
|
-
# @param password [String] API password
|
156
|
-
# @param auth_test_path [String] API path to test credential's validity
|
157
|
-
# @param session_path [String, nil] API session path
|
158
|
-
def set_auth_info(username, password, auth_test_path, session_path = nil)
|
159
|
-
@username = username
|
160
|
-
@password = password
|
161
|
-
@auth_test_path = auth_test_path
|
162
|
-
@session_path = @use_session ? session_path : nil
|
163
|
-
end
|
164
|
-
|
165
|
-
# Authenticate against the service.
|
166
|
-
#
|
167
|
-
# Calling this method will try to authenticate against API using
|
168
|
-
# credentials provided by #{set_auth_info} call.
|
169
|
-
# If authentication fails, # {AuthError} will be raised.
|
170
|
-
#
|
171
|
-
# @raise [AuthError] if credentials are invalid
|
172
|
-
def login
|
173
|
-
@session_path ? session_login : basic_login
|
174
|
-
end
|
175
|
-
|
176
|
-
# Sign out of the service.
|
177
|
-
def logout
|
178
|
-
# We bypass request here because we do not want any retries on 401
|
179
|
-
# when doing logout.
|
180
|
-
if @session_oid
|
181
|
-
params = prepare_request_params(:delete, @session_oid)
|
182
|
-
@connection.request(params)
|
183
|
-
@session_oid = nil
|
184
|
-
end
|
185
|
-
remove_headers([BASIC_AUTH_HEADER, TOKEN_AUTH_HEADER])
|
186
|
-
end
|
187
|
-
|
188
|
-
private
|
189
|
-
|
190
|
-
def do_request(method, path, data)
|
191
|
-
params = prepare_request_params(method, path, data)
|
192
|
-
r = @connection.request(params)
|
193
|
-
if r.status == 401
|
194
|
-
login
|
195
|
-
r = @connection.request(params)
|
196
|
-
end
|
197
|
-
Response.new(r.status, downcase_headers(r.data[:headers]), r.data[:body])
|
198
|
-
end
|
199
|
-
|
200
|
-
def downcase_headers(headers)
|
201
|
-
headers.each_with_object({}) { |(k, v), obj| obj[k.downcase] = v }
|
202
|
-
end
|
203
|
-
|
204
|
-
def prepare_request_params(method, path, data = nil)
|
205
|
-
params = { method: method, path: path }
|
206
|
-
if data
|
207
|
-
params[:body] = data.to_json
|
208
|
-
params[:headers] = @headers.merge("Content-Type" => "application/json")
|
209
|
-
else
|
210
|
-
params[:headers] = @headers
|
211
|
-
end
|
212
|
-
params
|
213
|
-
end
|
214
|
-
|
215
|
-
def session_login
|
216
|
-
# We bypass request here because we do not want any retries on 401
|
217
|
-
# when doing login.
|
218
|
-
params = prepare_request_params(:post, @session_path,
|
219
|
-
"UserName" => @username,
|
220
|
-
"Password" => @password)
|
221
|
-
r = @connection.request(params)
|
222
|
-
raise_invalid_auth_error unless r.status == 201
|
223
|
-
|
224
|
-
body = JSON.parse(r.data[:body])
|
225
|
-
headers = r.data[:headers]
|
226
|
-
|
227
|
-
add_headers(TOKEN_AUTH_HEADER => headers[TOKEN_AUTH_HEADER])
|
228
|
-
save_session_oid!(body, headers)
|
229
|
-
end
|
230
|
-
|
231
|
-
def save_session_oid!(body, headers)
|
232
|
-
@session_oid = body["@odata.id"] if body.key?("@odata.id")
|
233
|
-
return if @session_oid
|
234
|
-
|
235
|
-
return unless headers.key?(LOCATION_HEADER)
|
236
|
-
|
237
|
-
location = URI.parse(headers[LOCATION_HEADER])
|
238
|
-
@session_oid = [location.path, location.query].compact.join("?")
|
239
|
-
end
|
240
|
-
|
241
|
-
def basic_login
|
242
|
-
payload = Base64.encode64("#{@username}:#{@password}").strip
|
243
|
-
add_headers(BASIC_AUTH_HEADER => "Basic #{payload}")
|
244
|
-
return if auth_valid?
|
245
|
-
|
246
|
-
remove_headers([BASIC_AUTH_HEADER])
|
247
|
-
raise_invalid_auth_error
|
248
|
-
end
|
249
|
-
|
250
|
-
def raise_invalid_auth_error
|
251
|
-
raise AuthError, "Invalid credentials"
|
252
|
-
end
|
253
|
-
|
254
|
-
def auth_valid?
|
255
|
-
# We bypass request here because we do not want any retries on 401
|
256
|
-
# when checking authentication headers.
|
257
|
-
reset(@auth_test_path) # Do not want to see cached response
|
258
|
-
params = prepare_request_params(:get, @auth_test_path)
|
259
|
-
@connection.request(params).status == 200
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "uri"
|
5
|
-
|
6
|
-
module RedfishClient
|
7
|
-
# EventListener class can be used to stream events from Redfish service. It
|
8
|
-
# is a thin wrapper around SSE listener that does the dirty work of
|
9
|
-
# splitting each event into its EventRecords and reporting them as separate
|
10
|
-
# events.
|
11
|
-
class EventListener
|
12
|
-
# Create new EventListener instance.
|
13
|
-
#
|
14
|
-
# @param sse_client [ServerSentEvents::Client] SSE client
|
15
|
-
def initialize(sse_client)
|
16
|
-
@sse_client = sse_client
|
17
|
-
end
|
18
|
-
|
19
|
-
# Stream events from redfish service.
|
20
|
-
#
|
21
|
-
# Events that this method yields are actually EventRecords, extracted from
|
22
|
-
# the actual Redfish Event.
|
23
|
-
def listen
|
24
|
-
@sse_client.listen do |event|
|
25
|
-
split_event_into_records(event).each { |r| yield(r) }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def split_event_into_records(event)
|
32
|
-
JSON.parse(event.data).fetch("Events", [])
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,44 +0,0 @@
|
|
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
|
@@ -1,290 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
|
5
|
-
module RedfishClient
|
6
|
-
# Resource is basic building block of Redfish client and serves as a
|
7
|
-
# container for the data that is retrieved from the Redfish service.
|
8
|
-
#
|
9
|
-
# When we interact with the Redfish service, resource will wrap the data
|
10
|
-
# retrieved from the service API and offer us dot-notation accessors for
|
11
|
-
# values stored.
|
12
|
-
#
|
13
|
-
# Resource will also load any sub-resource on demand when we access it.
|
14
|
-
# For example, if we have a root Redfish resource stored in `root`,
|
15
|
-
# accessing `root.SessionService` will automatically fetch the appropriate
|
16
|
-
# resource from the API.
|
17
|
-
#
|
18
|
-
# In order to reduce the amount of requests being sent to the service,
|
19
|
-
# resource can also utilise caching connector. If we would like to get
|
20
|
-
# fresh values from the service, {#refresh} call will flush the cache and
|
21
|
-
# retrieve fresh data from the remote.
|
22
|
-
class Resource
|
23
|
-
# NoODataId error is raised when operation would need OpenData id of the
|
24
|
-
# resource to accomplish the task a hand.
|
25
|
-
class NoODataId < StandardError; end
|
26
|
-
|
27
|
-
# NoResource error is raised if the service cannot find requested
|
28
|
-
# resource.
|
29
|
-
class NoResource < StandardError; end
|
30
|
-
|
31
|
-
# Timeout error is raised if the async request is not handled in due time.
|
32
|
-
class Timeout < StandardError; end
|
33
|
-
|
34
|
-
# Headers, returned from the service when resource has been constructed.
|
35
|
-
#
|
36
|
-
# @return [Hash] resource headers
|
37
|
-
attr_reader :headers
|
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
|
-
|
45
|
-
# Create new resource.
|
46
|
-
#
|
47
|
-
# Resource can be created either by passing in OpenData identifier or
|
48
|
-
# supplying the content (hash). In the first case, connector will be used
|
49
|
-
# to fetch the resource data. In the second case, resource only wraps the
|
50
|
-
# passed-in hash and does no fetching.
|
51
|
-
#
|
52
|
-
# @param connector [RedfishClient::Connector] connector that will be used
|
53
|
-
# to fetch the resources
|
54
|
-
# @param oid [String] OpenData id of the resource
|
55
|
-
# @param raw [Hash] raw content to populate resource with
|
56
|
-
# @raise [NoResource] resource cannot be retrieved from the service
|
57
|
-
def initialize(connector, oid: nil, raw: nil)
|
58
|
-
@connector = connector
|
59
|
-
if oid
|
60
|
-
initialize_from_service(oid)
|
61
|
-
else
|
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)
|
83
|
-
end
|
84
|
-
raise Timeout, "Async operation did not terminate in allotted time"
|
85
|
-
end
|
86
|
-
|
87
|
-
# Access resource content.
|
88
|
-
#
|
89
|
-
# This function offers a way of accessing resource data in the same way
|
90
|
-
# that hash exposes its content.
|
91
|
-
#
|
92
|
-
# @param attr [String] key for accessing data
|
93
|
-
# @return associated value or `nil` if attr is missing
|
94
|
-
def [](attr)
|
95
|
-
build_resource(raw[attr])
|
96
|
-
end
|
97
|
-
|
98
|
-
# Safely access nested resource content.
|
99
|
-
#
|
100
|
-
# This function is an equivalent of safe navigation operator that can be
|
101
|
-
# used with arbitrary keys.
|
102
|
-
#
|
103
|
-
# Calling `res.dig("a", "b", "c")` is equivalent to `res.a&.b&.c` and
|
104
|
-
# `res["a"] && res["a"]["b"] && res["a"]["b"]["c"]`.
|
105
|
-
# @params keys [Array<Symbol, String>] sequence of keys to access
|
106
|
-
# @return associated value or `nil` if any key is missing
|
107
|
-
def dig(*keys)
|
108
|
-
keys.reduce(self) { |a, k| a.nil? ? nil : a[k] }
|
109
|
-
end
|
110
|
-
|
111
|
-
# Test if resource contains required key.
|
112
|
-
#
|
113
|
-
# @param name [String, Symbol] key name to test
|
114
|
-
# @return [Boolean] inclusion test result
|
115
|
-
def key?(name)
|
116
|
-
raw.key?(name.to_s)
|
117
|
-
end
|
118
|
-
|
119
|
-
# Convenience access for resource data.
|
120
|
-
#
|
121
|
-
# Calling `resource.Value` is exactly the same as `resource["Value"]`.
|
122
|
-
def method_missing(symbol, *_args, &_block)
|
123
|
-
self[symbol.to_s]
|
124
|
-
end
|
125
|
-
|
126
|
-
def respond_to_missing?(symbol, include_private = false)
|
127
|
-
key?(symbol.to_s) || super
|
128
|
-
end
|
129
|
-
|
130
|
-
# Pretty-print the wrapped content.
|
131
|
-
#
|
132
|
-
# @return [String] JSON-serialized raw data
|
133
|
-
def to_s
|
134
|
-
JSON.pretty_generate(raw)
|
135
|
-
end
|
136
|
-
|
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.
|
144
|
-
#
|
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)
|
156
|
-
end
|
157
|
-
|
158
|
-
# Issue a GET requests to the selected endpoint.
|
159
|
-
#
|
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)
|
176
|
-
end
|
177
|
-
|
178
|
-
# Issue a POST requests to the selected endpoint.
|
179
|
-
#
|
180
|
-
# By default, POST request will be sent to the path, stored in `@odata.id`
|
181
|
-
# field. Source field can be changed by specifying the `field` parameter
|
182
|
-
# when calling this function. Specifying the `path` argument will bypass
|
183
|
-
# the field lookup altogether and POST directly to the requested path.
|
184
|
-
#
|
185
|
-
# In order to avoid having to manually serialize data to JSON, this
|
186
|
-
# function call takes Hash as a payload and encodes it before sending it
|
187
|
-
# to the endpoint.
|
188
|
-
#
|
189
|
-
# If the resource has no lookup field, {NoODataId} error will be raised,
|
190
|
-
# since posting to non-networked resources makes no sense and probably
|
191
|
-
# indicates bug in library consumer.
|
192
|
-
#
|
193
|
-
# @param field [String, Symbol] path lookup field
|
194
|
-
# @param path [String] path to post to
|
195
|
-
# @param payload [Hash<String, >] data to send
|
196
|
-
# @return [RedfishClient::Response] response
|
197
|
-
# @raise [NoODataId] resource has no OpenData id
|
198
|
-
def post(field: "@odata.id", path: nil, payload: nil)
|
199
|
-
request(:post, field, path, payload)
|
200
|
-
end
|
201
|
-
|
202
|
-
# Issue a PATCH requests to the selected endpoint.
|
203
|
-
#
|
204
|
-
# Works exactly the same as the {post} method, but issued a PATCH request
|
205
|
-
# to the server.
|
206
|
-
#
|
207
|
-
# @param field [String, Symbol] path lookup field
|
208
|
-
# @param path [String] path to patch
|
209
|
-
# @param payload [Hash<String, >] data to send
|
210
|
-
# @return [RedfishClient::Response] response
|
211
|
-
# @raise [NoODataId] resource has no OpenData id
|
212
|
-
def patch(field: "@odata.id", path: nil, payload: nil)
|
213
|
-
request(:patch, field, path, payload)
|
214
|
-
end
|
215
|
-
|
216
|
-
# Issue a DELETE requests to the endpoint of the resource.
|
217
|
-
#
|
218
|
-
# If the resource has no `@odata.id` field, {NoODataId} error will be
|
219
|
-
# raised, since deleting non-networked resources makes no sense and
|
220
|
-
# probably indicates bug in library consumer.
|
221
|
-
#
|
222
|
-
# @return [RedfishClient::Response] response
|
223
|
-
# @raise [NoODataId] resource has no OpenData id
|
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"])
|
239
|
-
end
|
240
|
-
|
241
|
-
private
|
242
|
-
|
243
|
-
def initialize_from_service(oid)
|
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
|
259
|
-
|
260
|
-
def parse_fragment_string(fragment)
|
261
|
-
# /my/0/part -> ["my", "0", "part"]
|
262
|
-
fragment ? fragment.split("/").reject { |i| i == "" } : []
|
263
|
-
end
|
264
|
-
|
265
|
-
def get_path(field, path)
|
266
|
-
raise NoODataId if path.nil? && !key?(field)
|
267
|
-
path || raw[field]
|
268
|
-
end
|
269
|
-
|
270
|
-
def build_resource(data)
|
271
|
-
return nil if data.nil?
|
272
|
-
|
273
|
-
case data
|
274
|
-
when Hash then build_hash_resource(data)
|
275
|
-
when Array then data.collect { |d| build_resource(d) }
|
276
|
-
else data
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
def build_hash_resource(data)
|
281
|
-
if data.key?("@odata.id")
|
282
|
-
Resource.new(@connector, oid: data["@odata.id"])
|
283
|
-
else
|
284
|
-
Resource.new(@connector, raw: data)
|
285
|
-
end
|
286
|
-
rescue NoResource
|
287
|
-
nil
|
288
|
-
end
|
289
|
-
end
|
290
|
-
end
|
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "uri"
|
4
|
-
|
5
|
-
module RedfishClient
|
6
|
-
# Response struct.
|
7
|
-
#
|
8
|
-
# This struct is returned from the methods that interact with the remote API.
|
9
|
-
class Response
|
10
|
-
attr_reader :status
|
11
|
-
attr_reader :headers
|
12
|
-
attr_reader :body
|
13
|
-
|
14
|
-
def initialize(status, headers, body)
|
15
|
-
@status = status
|
16
|
-
@headers = headers
|
17
|
-
@body = body
|
18
|
-
end
|
19
|
-
|
20
|
-
# Returns wether the request is completed or ongoing. Be aware than completed
|
21
|
-
# doesn't mean completed successfully, and that you still need to check status
|
22
|
-
# for success or failure.
|
23
|
-
# @return [true] if the request was completed
|
24
|
-
def done?
|
25
|
-
status != 202
|
26
|
-
end
|
27
|
-
|
28
|
-
def monitor
|
29
|
-
return nil if done?
|
30
|
-
|
31
|
-
uri = URI.parse(headers["location"])
|
32
|
-
[uri.path, uri.query].compact.join("?")
|
33
|
-
end
|
34
|
-
|
35
|
-
def to_h
|
36
|
-
{ "status" => status, "headers" => headers, "body" => body }
|
37
|
-
end
|
38
|
-
|
39
|
-
def to_s
|
40
|
-
"Response[status=#{status}, headers=#{headers}, body='#{body}']"
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.from_hash(data)
|
44
|
-
new(*data.values_at("status", "headers", "body"))
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|