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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6ba76d56a2c09531c6e64bf19821d0b5f582dd70b16d1905aba16dc2b68df2e
4
- data.tar.gz: 530aebc45865ab6b64d2eb188c76f65dd1537f3e10d2b45c8e8dc29941dd86cb
3
+ metadata.gz: b77e06cc7ad353a5981721f72545f3ad266bf0b4a34ed08ef836ee52c48d8002
4
+ data.tar.gz: 2a674965dc162d9924eb6c0ef5050af50e173e3334c6fa5b0a59f66d9576921f
5
5
  SHA512:
6
- metadata.gz: 7671904be9a15568124d63ebff70d72e3b6f35a0adafc632e278637720aaae2b4e1ee1e29355997c47be9060cf6ce4daae6c8bfc7f2af8682e2c38f8f8e72f2b
7
- data.tar.gz: 074a32b18fb628befd56cc46977f3efbe431571cda082e4c6498d89a0dbc581debac11e07cbd2e5412437bb1d7cf653aef85b3e98ea071b0957bb43fb4c9eb4b
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
@@ -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
- @connector.request(method, get_path(field, path), payload)
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, payload: nil, headers: 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 patch(field: "@odata.id", path: nil, payload: nil, headers: nil)
221
- request(:patch, field, path, payload, headers)
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RedfishClient
4
- VERSION = "0.7.0"
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.7.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: 2025-12-16 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