redfish_client 0.6.2 → 0.8.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.
- checksums.yaml +4 -4
- data/lib/redfish_client/connector.rb +88 -3
- data/lib/redfish_client/resource.rb +78 -11
- data/lib/redfish_client/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b77e06cc7ad353a5981721f72545f3ad266bf0b4a34ed08ef836ee52c48d8002
|
|
4
|
+
data.tar.gz: 2a674965dc162d9924eb6c0ef5050af50e173e3334c6fa5b0a59f66d9576921f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8996d61ef36dad455a10de898c9e83108a447d897e4de9396102279ef6b1865c2acd7daca3f8adc067dca079e1e7cc953de7a653ffdbbd4a7702d11790d73de7
|
|
7
|
+
data.tar.gz: 275323b6abbcbfc4fc9b619da2ff48140e3ed272890167084e8678130f546c4ea3d2dafcfd43757597f1cc3dd2cc6d30d116a7fc056c74e1d50503ae7b4e178f
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "excon"
|
|
5
5
|
require "json"
|
|
6
|
+
require "logger"
|
|
6
7
|
|
|
7
8
|
require "redfish_client/nil_hash"
|
|
8
9
|
require "redfish_client/response"
|
|
@@ -113,13 +114,14 @@ module RedfishClient
|
|
|
113
114
|
request(:post, path, data)
|
|
114
115
|
end
|
|
115
116
|
|
|
116
|
-
# Issue PATCH requests to the service.
|
|
117
|
+
# Issue PATCH requests to the service with optional ETag support.
|
|
117
118
|
#
|
|
118
119
|
# @param path [String] path to the resource, relative to the base
|
|
119
120
|
# @param data [Hash] data to be sent over the socket
|
|
121
|
+
# @param options [Hash] optional parameters including :etag
|
|
120
122
|
# @return [Response] response object
|
|
121
|
-
def patch(path, data = nil)
|
|
122
|
-
|
|
123
|
+
def patch(path, data = nil, **options)
|
|
124
|
+
etag_handler(path, data, options[:etag])
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
# Issue DELETE requests to the service.
|
|
@@ -187,6 +189,89 @@ module RedfishClient
|
|
|
187
189
|
|
|
188
190
|
private
|
|
189
191
|
|
|
192
|
+
# ETag handler containing workarounds for PATCH requests with ETags.
|
|
193
|
+
# Based on sushy's _etag_handler implementation.
|
|
194
|
+
#
|
|
195
|
+
# @param path [String] path to the resource
|
|
196
|
+
# @param data [Hash] data to be sent
|
|
197
|
+
# @param etag [String, nil] ETag value
|
|
198
|
+
# @return [Response] response object
|
|
199
|
+
def etag_handler(path, data, etag)
|
|
200
|
+
# Guard clause: if no ETag provided, perform regular PATCH and return
|
|
201
|
+
return request(:patch, path, data) if etag.nil? || etag.empty?
|
|
202
|
+
|
|
203
|
+
logger = Logger.new($stdout)
|
|
204
|
+
logger.level = Logger::WARN
|
|
205
|
+
|
|
206
|
+
# Prepare headers with If-Match
|
|
207
|
+
headers_to_add = { "If-Match" => etag }
|
|
208
|
+
add_headers(headers_to_add)
|
|
209
|
+
|
|
210
|
+
begin
|
|
211
|
+
# First attempt with the provided ETag
|
|
212
|
+
response = request(:patch, path, data)
|
|
213
|
+
|
|
214
|
+
# Handle 412 Precondition Failed with retry logic.
|
|
215
|
+
# Some hardware vendors have non-standard Redfish implementations
|
|
216
|
+
# that incorrectly handle ETags (e.g., rejecting weak ETags or
|
|
217
|
+
# requiring ETags to be omitted even when provided correctly).
|
|
218
|
+
# To work around these vendor-specific issues, we retry with:
|
|
219
|
+
# 1. Converting weak ETag (W/"...") to strong ETag ("...")
|
|
220
|
+
# 2. Removing the If-Match header entirely
|
|
221
|
+
# This approach is based on similar workarounds implemented in
|
|
222
|
+
# the Sushy library:
|
|
223
|
+
# https://github.com/openstack/sushy
|
|
224
|
+
# Other statuses (success or errors like 400, 500) should be
|
|
225
|
+
# returned as-is for the caller to handle appropriately.
|
|
226
|
+
unless response.status == 412
|
|
227
|
+
return response
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
logger.warn("Initial request with eTag failed: HTTP 412")
|
|
231
|
+
|
|
232
|
+
# Check for weak ETag (W/"...")
|
|
233
|
+
weak_etag_pattern = /^(W\/)(".+")$/
|
|
234
|
+
match = weak_etag_pattern.match(etag)
|
|
235
|
+
|
|
236
|
+
if match
|
|
237
|
+
logger.info("Weak eTag provided with original request to #{path}. " \
|
|
238
|
+
"Attempting conversion to strong eTag and re-trying.")
|
|
239
|
+
|
|
240
|
+
# Try with strong ETag (remove W/ prefix)
|
|
241
|
+
strong_etag = match[2]
|
|
242
|
+
remove_headers(["If-Match"])
|
|
243
|
+
add_headers("If-Match" => strong_etag)
|
|
244
|
+
|
|
245
|
+
response = request(:patch, path, data)
|
|
246
|
+
|
|
247
|
+
unless response.status == 412
|
|
248
|
+
return response
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
logger.warn("Request to #{path} with weak eTag converted to " \
|
|
252
|
+
"strong eTag also failed. Making the final attempt " \
|
|
253
|
+
"with no eTag specified.")
|
|
254
|
+
else
|
|
255
|
+
# ETag is strong, retry without it
|
|
256
|
+
logger.warn("Strong eTag provided - retrying request to #{path} " \
|
|
257
|
+
"with eTag removed.")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Final attempt without If-Match header
|
|
261
|
+
remove_headers(["If-Match"])
|
|
262
|
+
response = request(:patch, path, data)
|
|
263
|
+
|
|
264
|
+
if response.status == 412
|
|
265
|
+
logger.error("Final re-try with eTag removed has also failed with HTTP 412")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
response
|
|
269
|
+
ensure
|
|
270
|
+
# Clean up headers
|
|
271
|
+
remove_headers(headers_to_add.keys) unless headers_to_add.empty?
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
190
275
|
def do_request(method, path, data)
|
|
191
276
|
params = prepare_request_params(method, path, data)
|
|
192
277
|
r = @connection.request(params)
|
|
@@ -42,6 +42,23 @@ module RedfishClient
|
|
|
42
42
|
# @return [Hash] resource raw data
|
|
43
43
|
attr_reader :raw
|
|
44
44
|
|
|
45
|
+
# Get ETag from the response headers or resource property.
|
|
46
|
+
#
|
|
47
|
+
# Redfish services may provide ETag in two ways:
|
|
48
|
+
# 1. HTTP ETag header (preferred)
|
|
49
|
+
# 2. @odata.etag property in the resource (fallback)
|
|
50
|
+
#
|
|
51
|
+
# This method checks both locations, prioritizing the HTTP header.
|
|
52
|
+
#
|
|
53
|
+
# @return [String, nil] ETag value or nil if not present
|
|
54
|
+
def etag
|
|
55
|
+
# Prefer HTTP header (used for If-Match header)
|
|
56
|
+
return @headers["etag"] if @headers&.key?("etag")
|
|
57
|
+
|
|
58
|
+
# Fallback to @odata.etag property
|
|
59
|
+
raw["@odata.etag"]
|
|
60
|
+
end
|
|
61
|
+
|
|
45
62
|
# Create new resource.
|
|
46
63
|
#
|
|
47
64
|
# Resource can be created either by passing in OpenData identifier or
|
|
@@ -149,10 +166,24 @@ module RedfishClient
|
|
|
149
166
|
# @param method [Symbol] HTTP method (:get, :post, :patch or :delete)
|
|
150
167
|
# @param field [String, Symbol] path lookup field
|
|
151
168
|
# @param path [String] path to post to
|
|
169
|
+
# @param payload Hash<String, >] data to send
|
|
170
|
+
# @param headers [Hash<String, String>] additional headers for this request only
|
|
171
|
+
# @param etag [String, nil] optional ETag value for If-Match header
|
|
152
172
|
# @return [RedfishClient::Response] response
|
|
153
173
|
# @raise [NoODataId] resource has no OpenData id
|
|
154
|
-
def request(method, field, path, payload = nil)
|
|
155
|
-
@connector.
|
|
174
|
+
def request(method, field, path, payload = nil, headers = nil, etag = nil)
|
|
175
|
+
@connector.add_headers(headers) if headers&.any?
|
|
176
|
+
target_path = get_path(field, path)
|
|
177
|
+
|
|
178
|
+
# Use etag-aware patch method when etag is provided
|
|
179
|
+
if method == :patch && etag
|
|
180
|
+
# Forward ETag value from caller to Connector#patch as keyword argument
|
|
181
|
+
@connector.patch(target_path, payload, etag: etag)
|
|
182
|
+
else
|
|
183
|
+
@connector.request(method, target_path, payload)
|
|
184
|
+
end
|
|
185
|
+
ensure
|
|
186
|
+
@connector.remove_headers(headers) if headers&.any?
|
|
156
187
|
end
|
|
157
188
|
|
|
158
189
|
# Issue a GET requests to the selected endpoint.
|
|
@@ -169,10 +200,11 @@ module RedfishClient
|
|
|
169
200
|
#
|
|
170
201
|
# @param field [String, Symbol] path lookup field
|
|
171
202
|
# @param path [String] path to post to
|
|
203
|
+
# @param headers [Hash<String, String>] additional headers for this request only
|
|
172
204
|
# @return [RedfishClient::Response] response
|
|
173
205
|
# @raise [NoODataId] resource has no OpenData id
|
|
174
|
-
def get(field: "@odata.id", path: nil)
|
|
175
|
-
request(:get, field, path)
|
|
206
|
+
def get(field: "@odata.id", path: nil, headers: nil)
|
|
207
|
+
request(:get, field, path, nil, headers)
|
|
176
208
|
end
|
|
177
209
|
|
|
178
210
|
# Issue a POST requests to the selected endpoint.
|
|
@@ -193,10 +225,11 @@ module RedfishClient
|
|
|
193
225
|
# @param field [String, Symbol] path lookup field
|
|
194
226
|
# @param path [String] path to post to
|
|
195
227
|
# @param payload [Hash<String, >] data to send
|
|
228
|
+
# @param headers [Hash<String, String>] additional headers for this request only
|
|
196
229
|
# @return [RedfishClient::Response] response
|
|
197
230
|
# @raise [NoODataId] resource has no OpenData id
|
|
198
|
-
def post(field: "@odata.id", path: nil, payload: nil)
|
|
199
|
-
request(:post, field, path, payload)
|
|
231
|
+
def post(field: "@odata.id", path: nil, payload: nil, headers: nil)
|
|
232
|
+
request(:post, field, path, payload, headers)
|
|
200
233
|
end
|
|
201
234
|
|
|
202
235
|
# Issue a PATCH requests to the selected endpoint.
|
|
@@ -207,10 +240,40 @@ module RedfishClient
|
|
|
207
240
|
# @param field [String, Symbol] path lookup field
|
|
208
241
|
# @param path [String] path to patch
|
|
209
242
|
# @param payload [Hash<String, >] data to send
|
|
243
|
+
# @param headers [Hash<String, String>] additional headers for this request only
|
|
244
|
+
# @param etag [String, nil] optional ETag value for If-Match header
|
|
245
|
+
# @return [RedfishClient::Response] response
|
|
246
|
+
# @raise [NoODataId] resource has no OpenData id
|
|
247
|
+
def patch(field: "@odata.id", path: nil, payload: nil, headers: nil, etag: nil)
|
|
248
|
+
request(:patch, field, path, payload, headers, etag)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Issue a PATCH request using the ETag from this resource.
|
|
252
|
+
#
|
|
253
|
+
# This is a convenience method that uses the ETag value obtained when
|
|
254
|
+
# this resource was retrieved from the service. The resource must be
|
|
255
|
+
# fetched via GET (e.g., using `client.find()`) before calling this
|
|
256
|
+
# method, so that the ETag is available in the response headers.
|
|
257
|
+
#
|
|
258
|
+
# If no ETag is present in the headers, this method will perform a
|
|
259
|
+
# regular PATCH without the If-Match header.
|
|
260
|
+
#
|
|
261
|
+
# @example Basic usage
|
|
262
|
+
# # First, retrieve the resource (GET request with ETag in response)
|
|
263
|
+
# system = client.find("/redfish/v1/Systems/1")
|
|
264
|
+
#
|
|
265
|
+
# # Then, update with ETag validation (PATCH with If-Match header)
|
|
266
|
+
# system.patch_if_match({ "AssetTag" => "Server-001" })
|
|
267
|
+
#
|
|
268
|
+
# @param payload [Hash<String, >] data to send
|
|
269
|
+
# @param field [String, Symbol] path lookup field
|
|
270
|
+
# @param path [String] path to patch
|
|
271
|
+
# @param headers [Hash<String, String>] additional headers for this request only
|
|
210
272
|
# @return [RedfishClient::Response] response
|
|
211
273
|
# @raise [NoODataId] resource has no OpenData id
|
|
212
|
-
def
|
|
213
|
-
|
|
274
|
+
def patch_if_match(payload, field: "@odata.id", path: nil, headers: nil)
|
|
275
|
+
current_etag = etag
|
|
276
|
+
patch(field: field, path: path, payload: payload, headers: headers, etag: current_etag)
|
|
214
277
|
end
|
|
215
278
|
|
|
216
279
|
# Issue a DELETE requests to the endpoint of the resource.
|
|
@@ -219,16 +282,20 @@ module RedfishClient
|
|
|
219
282
|
# raised, since deleting non-networked resources makes no sense and
|
|
220
283
|
# probably indicates bug in library consumer.
|
|
221
284
|
#
|
|
285
|
+
# @param field [String, Symbol] path lookup field
|
|
286
|
+
# @param path [String] path to patch
|
|
287
|
+
# @param payload [Hash<String, >] data to send
|
|
288
|
+
# @param headers [Hash<String, String>] additional headers for this request only
|
|
222
289
|
# @return [RedfishClient::Response] response
|
|
223
290
|
# @raise [NoODataId] resource has no OpenData id
|
|
224
|
-
def delete(field: "@odata.id", path: nil, payload: nil)
|
|
225
|
-
request(:delete, field, path, payload)
|
|
291
|
+
def delete(field: "@odata.id", path: nil, payload: nil, headers: nil)
|
|
292
|
+
request(:delete, field, path, payload, headers)
|
|
226
293
|
end
|
|
227
294
|
|
|
228
295
|
# Refresh resource content from the API
|
|
229
296
|
#
|
|
230
297
|
# Caling this method will ensure that the resource data is in sync with
|
|
231
|
-
# the
|
|
298
|
+
# the Redfish API, invalidating any caches as necessary.
|
|
232
299
|
def refresh
|
|
233
300
|
return unless self["@odata.id"]
|
|
234
301
|
|
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
|
+
version: 0.8.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:
|
|
11
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: excon
|