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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d565357ebc33ecafd73baf529638da1b24e03ed80c1272f769278f8d65822982
4
- data.tar.gz: 21724f92efa1c966eb1fcd361ae5fbcc34a5c3f4b13d76e261f325f382fce825
3
+ metadata.gz: b77e06cc7ad353a5981721f72545f3ad266bf0b4a34ed08ef836ee52c48d8002
4
+ data.tar.gz: 2a674965dc162d9924eb6c0ef5050af50e173e3334c6fa5b0a59f66d9576921f
5
5
  SHA512:
6
- metadata.gz: fe37c7362f8365175014ec302c66aad65d968bb18fac641cdbbe97fc0ca7842330bd4dbbbac02eff150d90ea50148922f73444eaf3bfb89a4bffb92d4f37fec3
7
- data.tar.gz: 29a334780905e1ffea4fd1a9798d8b51aa9120e2aef7031e6e4d0c56d60536759a4d405c5e3cec39a008c0764b72dc3c89b8ad4b8720cc1b076cceb26f61ee6d
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
- request(:patch, path, data)
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.request(method, get_path(field, path), payload)
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 patch(field: "@odata.id", path: nil, payload: nil)
213
- request(:patch, field, path, payload)
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 Redfis API, invalidating any caches as necessary.
298
+ # the Redfish API, invalidating any caches as necessary.
232
299
  def refresh
233
300
  return unless self["@odata.id"]
234
301
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RedfishClient
4
- VERSION = "0.6.2"
4
+ VERSION = "0.8.0"
5
5
  end
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.6.2
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: 2025-03-04 00:00:00.000000000 Z
11
+ date: 2026-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon