redfish_client 0.7.0 → 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 +60 -5
- 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
|
|
@@ -151,11 +168,20 @@ module RedfishClient
|
|
|
151
168
|
# @param path [String] path to post to
|
|
152
169
|
# @param payload Hash<String, >] data to send
|
|
153
170
|
# @param headers [Hash<String, String>] additional headers for this request only
|
|
171
|
+
# @param etag [String, nil] optional ETag value for If-Match header
|
|
154
172
|
# @return [RedfishClient::Response] response
|
|
155
173
|
# @raise [NoODataId] resource has no OpenData id
|
|
156
|
-
def request(method, field, path, payload = nil, headers = nil)
|
|
174
|
+
def request(method, field, path, payload = nil, headers = nil, etag = nil)
|
|
157
175
|
@connector.add_headers(headers) if headers&.any?
|
|
158
|
-
|
|
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
|
|
159
185
|
ensure
|
|
160
186
|
@connector.remove_headers(headers) if headers&.any?
|
|
161
187
|
end
|
|
@@ -202,7 +228,7 @@ module RedfishClient
|
|
|
202
228
|
# @param headers [Hash<String, String>] additional headers for this request only
|
|
203
229
|
# @return [RedfishClient::Response] response
|
|
204
230
|
# @raise [NoODataId] resource has no OpenData id
|
|
205
|
-
def post(field: "@odata.id", path: nil,
|
|
231
|
+
def post(field: "@odata.id", path: nil, payload: nil, headers: nil)
|
|
206
232
|
request(:post, field, path, payload, headers)
|
|
207
233
|
end
|
|
208
234
|
|
|
@@ -215,10 +241,39 @@ module RedfishClient
|
|
|
215
241
|
# @param path [String] path to patch
|
|
216
242
|
# @param payload [Hash<String, >] data to send
|
|
217
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
|
|
218
272
|
# @return [RedfishClient::Response] response
|
|
219
273
|
# @raise [NoODataId] resource has no OpenData id
|
|
220
|
-
def
|
|
221
|
-
|
|
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)
|
|
222
277
|
end
|
|
223
278
|
|
|
224
279
|
# Issue a DELETE requests to the endpoint of the resource.
|
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
|