idrac 0.9.0 → 0.9.4

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: 40d8abda65007546224a817d15c735d6b0f4fb35f7d4aba6a429bf1f4d92bc98
4
- data.tar.gz: 8a33504cada67707e4f5d38ea8af248d68c6de3d91bf9f03c867ec2042617c9b
3
+ metadata.gz: 54237d0e5e104838b8770088791b1896b11928d0092572035ee05117de1d6733
4
+ data.tar.gz: 802a048dcf46d8d9a1eab4626be4425269ee58d0012b61f1ac167b44a8b1def9
5
5
  SHA512:
6
- metadata.gz: 451ec8c73047bb0af4abf66e6b8139b7c84ea4f5e94c3391aa791b669c5e7cd95bc6aaa980ea0b37978b7f464cf6b9598072efdd767a4714bd542d1345f705cc
7
- data.tar.gz: 98772256156d10521bffc87e752ef2249d35c865af29a3ad74743af283dd74ac0fbb19cff18767978fa06b2ef556dc57ee847b74381f6360ab462d829d021295
6
+ metadata.gz: 95ff7c2f6e83194632b4ceb9febd251f01332f39271c02bfc2e0f35e677159b873b7ff74a8906f5b293af855aa2fb3f69ed4e73242be8ca704f6346da507c9a8
7
+ data.tar.gz: 94f8e2b5ab66ff0cf428bdb1903d3ba3d38d9b3e2002323e36a866ffb4461ae22a2766861550d4daf0fe74b274e2991ef9280fd9732581af9d0b86d3209b7047
data/README.md CHANGED
@@ -291,6 +291,13 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
291
291
 
292
292
  ## Changelog
293
293
 
294
+ ### Version 0.9.1
295
+ - **Code Simplification**: Removed redundant `handle_response` calls throughout the codebase
296
+ - `authenticated_request` now automatically handles error responses (status >= 400)
297
+ - Removed 15 lines of redundant error handling code across storage.rb, power.rb, and virtual_media.rb
298
+ - Simplified response parsing in `get_power_state` and `get_power_usage_watts` methods
299
+ - Improved code maintainability by relying on centralized error handling
300
+
294
301
  ### Version 0.9.0
295
302
  - **Automatic Retry Handling**: ServiceTemporarilyUnavailable (503) errors now automatically retry with iDRAC-specified delay
296
303
  - **Simplified API**: `authenticated_request` now returns response body string by default instead of response object
data/lib/idrac/boot.rb CHANGED
@@ -165,8 +165,7 @@ module IDRAC
165
165
  response = authenticated_request(
166
166
  :patch,
167
167
  "/redfish/v1/Systems/System.Embedded.1",
168
- body: body.to_json,
169
- headers: { 'Content-Type': 'application/json' }
168
+ body: body.to_json
170
169
  )
171
170
 
172
171
  if response.status.between?(200, 299)
@@ -190,8 +189,7 @@ module IDRAC
190
189
  response = authenticated_request(
191
190
  :patch,
192
191
  "/redfish/v1/Systems/System.Embedded.1",
193
- body: body.to_json,
194
- headers: { 'Content-Type': 'application/json' }
192
+ body: body.to_json
195
193
  )
196
194
 
197
195
  if response.status.between?(200, 299)
@@ -215,8 +213,7 @@ module IDRAC
215
213
  response = authenticated_request(
216
214
  :patch,
217
215
  "/redfish/v1/Systems/System.Embedded.1",
218
- body: body.to_json,
219
- headers: { 'Content-Type': 'application/json' }
216
+ body: body.to_json
220
217
  )
221
218
 
222
219
  if response.status.between?(200, 299)
@@ -312,10 +309,9 @@ module IDRAC
312
309
  end
313
310
 
314
311
  response = authenticated_request(
315
- :patch,
312
+ :patch,
316
313
  "/redfish/v1/Systems/System.Embedded.1/Bios/Settings",
317
- body: payload.to_json,
318
- headers: { 'Content-Type': 'application/json' }
314
+ body: payload.to_json
319
315
  )
320
316
 
321
317
  wait_for_job(response.headers["location"])
@@ -396,10 +392,9 @@ module IDRAC
396
392
 
397
393
  # Set boot order
398
394
  response = authenticated_request(
399
- :patch,
395
+ :patch,
400
396
  "/redfish/v1/Systems/System.Embedded.1",
401
- body: { "Boot": { "BootOrder": [boot_id] } }.to_json,
402
- headers: { 'Content-Type': 'application/json' }
397
+ body: { "Boot": { "BootOrder": [boot_id] } }.to_json
403
398
  )
404
399
 
405
400
  if response.status.between?(200, 299)
@@ -495,10 +490,9 @@ module IDRAC
495
490
  # Configure BIOS settings
496
491
  def configure_bios_settings(settings)
497
492
  response = authenticated_request(
498
- :patch,
493
+ :patch,
499
494
  "/redfish/v1/Systems/System.Embedded.1/Bios/Settings",
500
- body: { "Attributes": settings }.to_json,
501
- headers: { 'Content-Type': 'application/json' }
495
+ body: { "Attributes": settings }.to_json
502
496
  )
503
497
 
504
498
  if response.status.between?(200, 299)
@@ -676,10 +670,9 @@ module IDRAC
676
670
  params["HostPowerState"] = reboot ? "On" : "Off"
677
671
 
678
672
  response = authenticated_request(
679
- :post,
673
+ :post,
680
674
  "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
681
- body: params.to_json,
682
- headers: { 'Content-Type': 'application/json' }
675
+ body: params.to_json
683
676
  )
684
677
 
685
678
  task = wait_for_task(response.headers["location"])
data/lib/idrac/client.rb CHANGED
@@ -86,14 +86,14 @@ module IDRAC
86
86
  end
87
87
  end
88
88
 
89
- # Login to iDRAC
89
+ def authenticated?
90
+ @direct_mode || session.x_auth_token
91
+ end
92
+
93
+ # Login to iDRAC (no-op if already authenticated)
90
94
  def login
91
- # If we're in direct mode, skip login attempts
92
- if @direct_mode
93
- debug "Using direct mode (Basic Auth) for all requests", 1, :light_yellow
94
- return true
95
- end
96
-
95
+ return true if authenticated?
96
+
97
97
  # Try to create a Redfish session
98
98
  if session.create
99
99
  debug "Successfully logged in to iDRAC using Redfish session", 1, :green
@@ -105,6 +105,11 @@ module IDRAC
105
105
  end
106
106
  end
107
107
 
108
+ # Ensure we're authenticated before making requests (idempotent)
109
+ def ensure_authenticated!
110
+ login unless authenticated?
111
+ end
112
+
108
113
  # Logout from iDRAC
109
114
  def logout
110
115
  session.delete if session.x_auth_token
@@ -30,8 +30,7 @@ module IDRAC
30
30
  # Ensure we have a client
31
31
  raise Error, "Client is required for firmware update" unless client
32
32
 
33
- # Login to iDRAC
34
- client.login unless client.instance_variable_get(:@session_id)
33
+ client.ensure_authenticated!
35
34
 
36
35
  # Upload firmware file
37
36
  job_id = upload_firmware(firmware_path)
@@ -563,10 +562,7 @@ module IDRAC
563
562
  update_response = client.authenticated_request(
564
563
  :post,
565
564
  "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
566
- {
567
- headers: { 'Content-Type' => 'application/json' },
568
- body: simple_update_payload.to_json
569
- }
565
+ body: simple_update_payload.to_json
570
566
  )
571
567
 
572
568
  if update_response.status != 202 && update_response.status != 200
data/lib/idrac/jobs.rb CHANGED
@@ -75,10 +75,9 @@ module IDRAC
75
75
  payload = { "JobID" => "JID_CLEARALL_FORCE" }
76
76
 
77
77
  response = authenticated_request(
78
- :post,
79
- path,
80
- body: payload.to_json,
81
- headers: { 'Content-Type' => 'application/json' }
78
+ :post,
79
+ path,
80
+ body: payload.to_json
82
81
  )
83
82
 
84
83
  if response.status.between?(200, 299)
@@ -90,10 +89,9 @@ module IDRAC
90
89
  retries = 60 # ~10 minutes with 10s sleep
91
90
  while retries > 0
92
91
  lc_response = authenticated_request(
93
- :post,
92
+ :post,
94
93
  '/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.GetRemoteServicesAPIStatus',
95
- body: {}.to_json,
96
- headers: { 'Content-Type': 'application/json' }
94
+ body: {}.to_json
97
95
  )
98
96
 
99
97
  if lc_response.status.between?(200, 299)
data/lib/idrac/license.rb CHANGED
@@ -1,359 +1,134 @@
1
1
  module IDRAC
2
2
  module License
3
3
  # Gets the license information from the iDRAC
4
- # @return [Hash] License details
4
+ # @return [Hash, nil] License details
5
5
  def license_info
6
- # Try the standard license endpoint first (works in iDRAC 9+)
7
- response = authenticated_request(:get, "/redfish/v1/LicenseService/Licenses")
8
-
9
- if response.status == 200
10
- license_data = JSON.parse(response.body)
11
- debug "License collection: #{license_data}", 2
12
-
13
- # Check if there are any license entries
14
- if !license_data["Members"] || license_data["Members"].empty?
15
- debug "No licenses found", 1, :yellow
16
- return try_dell_oem_license_path()
17
- end
18
-
19
- # Get the first license in the list
20
- license_uri = license_data["Members"][0]["@odata.id"]
21
- debug "Using license URI: #{license_uri}", 2
22
-
23
- # Get detailed license information
24
- license_response = authenticated_request(:get, license_uri)
25
- if license_response.status != 200
26
- debug "Failed to retrieve license details: #{license_response.status}", 1, :red
27
- return try_dell_oem_license_path()
6
+ # Try standard endpoint first (iDRAC 9+)
7
+ if (data = safe_get("/redfish/v1/LicenseService/Licenses"))
8
+ if data["Members"]&.any?
9
+ license_uri = data["Members"][0]["@odata.id"]
10
+ return safe_get(license_uri) || try_dell_oem_license_path
28
11
  end
29
-
30
- license_details = JSON.parse(license_response.body)
31
- debug "License details: #{license_details}", 2
32
-
33
- return license_details
34
- else
35
- # The endpoint is not available (probably iDRAC 8)
36
- debug "Standard license endpoint failed: #{response.status}, trying Dell OEM path", 1, :yellow
37
- return try_dell_oem_license_path()
38
12
  end
13
+
14
+ try_dell_oem_license_path
39
15
  end
40
16
 
41
- # Extracts the iDRAC version from the license description or server header
42
- # @return [Integer, nil] The license version (e.g. 9) or nil if not found
17
+ # Extracts the iDRAC generation (e.g. 8, 9) from license or server header
18
+ # @return [Integer, nil]
43
19
  def license_version
44
- # Use memoization to cache the result and avoid multiple API calls
45
20
  @license_version ||= compute_license_version
46
21
  end
47
-
48
- # Clear the cached license version (useful if iDRAC state changes)
22
+
49
23
  def clear_license_version_cache
50
24
  @license_version = nil
51
25
  end
52
-
26
+
53
27
  private
54
-
28
+
29
+ # GET that returns parsed JSON on 200, nil otherwise. Never raises on 4xx.
30
+ def safe_get(path)
31
+ response = authenticated_request(:get, path) { |r| r }
32
+ return nil unless response.status == 200
33
+ JSON.parse(response.body)
34
+ rescue JSON::ParserError
35
+ nil
36
+ end
37
+
55
38
  def compute_license_version
56
- # First try to get from license info
57
39
  license = license_info
58
40
  if license
59
- # Check the Description field, which often contains the version
60
- # Example: "iDRAC9 Enterprise License"
61
- if license["Description"]&.match(/iDRAC(\d+)/i)
62
- version = license["Description"].match(/iDRAC(\d+)/i)[1].to_i
63
- debug "Found license version from Description: #{version}", 1
64
- return version
65
- end
66
-
67
- # Try alternative fields if Description didn't work
68
- if license["Name"]&.match(/iDRAC(\d+)/i)
69
- version = license["Name"].match(/iDRAC(\d+)/i)[1].to_i
70
- debug "Found license version from Name: #{version}", 1
71
- return version
72
- end
73
-
74
- # For Dell OEM license response format
75
- if license["LicenseDescription"]&.match(/iDRAC(\d+)/i)
76
- version = license["LicenseDescription"].match(/iDRAC(\d+)/i)[1].to_i
77
- debug "Found license version from LicenseDescription: #{version}", 1
78
- return version
41
+ %w[Description Name LicenseDescription].each do |field|
42
+ if license[field]&.match(/iDRAC(\d+)/i)
43
+ return $1.to_i
44
+ end
79
45
  end
80
46
  end
81
-
82
- # If license info failed or didn't have version info, try to get from server header
83
- # Make a simple request to check the server header (often contains iDRAC version)
84
- response = authenticated_request(:get, "/redfish/v1")
85
- if response.headers["server"] && response.headers["server"].match(/iDRAC\/(\d+)/i)
86
- version = response.headers["server"].match(/iDRAC\/(\d+)/i)[1].to_i
87
- debug "Found license version from server header: #{version}", 1
88
- return version
47
+
48
+ # Fall back to server header
49
+ response = authenticated_request(:get, "/redfish/v1") { |r| r }
50
+ if response.headers["server"]&.match(/iDRAC\/(\d+)/i)
51
+ return $1.to_i
89
52
  end
90
-
91
- debug "Could not determine license version from license info or server header", 1, :yellow
53
+
92
54
  nil
93
55
  end
94
-
95
- # Attempt to get license information using Dell OEM extension path (for iDRAC 8)
96
- # @return [Hash, nil] License info or nil if not found
56
+
97
57
  def try_dell_oem_license_path
98
- # Try several potential Dell license paths (order matters - most likely first)
99
- dell_license_paths = [
100
- "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLicenseManagementService/Licenses",
101
- "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes", # iDRAC attributes might contain license info
102
- "/redfish/v1/Managers/iDRAC.Embedded.1" # Manager entity might have license info embedded
103
- ]
104
-
105
- dell_license_paths.each do |path|
106
- response = authenticated_request(:get, path)
107
-
108
- if response.status == 200
109
- debug "Found valid Dell license path: #{path}", 2
110
- data = JSON.parse(response.body)
111
-
112
- # Check for license info in this response based on the path
113
- if path.include?("DellLicenseManagementService")
114
- return handle_dell_license_service_response(data)
115
- elsif path.include?("Attributes")
116
- return handle_dell_attributes_response(data)
117
- elsif path.include?("iDRAC.Embedded.1") && !path.include?("Attributes")
118
- return handle_dell_manager_response(data)
58
+ # Try Dell OEM license service (iDRAC 8)
59
+ if (data = safe_get("/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLicenseManagementService/Licenses"))
60
+ if data["Members"]&.any?
61
+ license_uri = data["Members"][0]["@odata.id"]
62
+ if (details = safe_get(license_uri))
63
+ return {
64
+ "Id" => details["EntitlementID"] || "iDRAC-License",
65
+ "Description" => details["LicenseDescription"] || "iDRAC License",
66
+ "Name" => details["LicenseDescription"] || "iDRAC License",
67
+ "LicenseType" => details["LicenseType"] || license_type_from(details["LicenseDescription"]),
68
+ "Status" => { "Health" => "OK" },
69
+ "Removable" => true
70
+ }
119
71
  end
120
- else
121
- debug "Dell path #{path} response status: #{response.status}", 3
122
72
  end
123
73
  end
124
-
125
- # If we couldn't find any API path that works, try the service tag detection method
126
- service_tag = get_service_tag
127
- if service_tag
128
- # Service tag is often used to indicate Enterprise licenses on Dell systems
129
- license_type = determine_license_type()
130
- return {
131
- "Id" => "iDRAC-License",
132
- "Description" => "iDRAC8 #{license_type} License",
133
- "Name" => "iDRAC License",
134
- "LicenseType" => license_type,
135
- "Status" => { "Health" => "OK" },
136
- "Removable" => false,
137
- "EntitlementID" => service_tag # Dell often uses service tag as entitlement ID
138
- }
139
- end
140
-
141
- # Fall back to feature detection if all else fails
142
- debug "All Dell OEM license paths failed, using fallback detection", 1, :yellow
143
- return create_fallback_license_info()
144
- end
145
-
146
- # Handle response from Dell License Management Service
147
- def handle_dell_license_service_response(data)
148
- # Check if there are any license entries
149
- if !data["Members"] || data["Members"].empty?
150
- debug "No licenses found in Dell OEM path", 1, :yellow
151
- return create_fallback_license_info(use_basic: true)
152
- end
153
-
154
- # Get the first license in the list
155
- license_uri = data["Members"][0]["@odata.id"]
156
- debug "Using Dell OEM license URI: #{license_uri}", 2
157
-
158
- # Get detailed license information
159
- license_response = authenticated_request(:get, license_uri)
160
- if license_response.status != 200
161
- debug "Failed to retrieve Dell OEM license details: #{license_response.status}", 1, :red
162
- return create_fallback_license_info(use_basic: true)
163
- end
164
-
165
- dell_license = JSON.parse(license_response.body)
166
- debug "Dell OEM license details: #{dell_license}", 2
167
-
168
- # Convert Dell OEM license format to standard format
169
- license_info = {
170
- "Id" => dell_license["EntitlementID"] || "iDRAC-License",
171
- "Description" => dell_license["LicenseDescription"] || "iDRAC License",
172
- "Name" => dell_license["LicenseDescription"] || "iDRAC License",
173
- "LicenseType" => dell_license["LicenseType"] || get_license_type_from_description(dell_license["LicenseDescription"]),
174
- "Status" => { "Health" => "OK" },
175
- "Removable" => true
176
- }
177
-
178
- return license_info
179
- end
180
-
181
- # Handle response from Dell Manager attributes
182
- def handle_dell_attributes_response(data)
183
- # Look for license information in attributes
184
- if data["Attributes"] && (
185
- data["Attributes"]["LicensableDevice.1.LicenseInfo.1"] ||
186
- data["Attributes"]["System.ServerOS.1.OSName"] ||
187
- data["Attributes"]["iDRAC.Info.1.LicensingInfo"]
188
- )
189
-
190
- license_info = data["Attributes"]["LicensableDevice.1.LicenseInfo.1"] ||
191
- data["Attributes"]["iDRAC.Info.1.LicensingInfo"]
192
-
193
- if license_info
194
- license_type = license_info.include?("Enterprise") ? "Enterprise" :
195
- license_info.include?("Express") ? "Express" : "Basic"
196
-
197
- return {
198
- "Id" => "iDRAC-License",
199
- "Description" => "iDRAC8 #{license_type} License",
200
- "Name" => "iDRAC License",
201
- "LicenseType" => license_type,
202
- "Status" => { "Health" => "OK" },
203
- "Removable" => false
204
- }
205
- end
74
+
75
+ # Try manager attributes
76
+ if (data = safe_get("/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"))
77
+ attr = data.dig("Attributes", "LicensableDevice.1.LicenseInfo.1") ||
78
+ data.dig("Attributes", "iDRAC.Info.1.LicensingInfo")
79
+ return build_license_hash(license_type_from(attr)) if attr
206
80
  end
207
-
208
- # If no license attributes found, fall back to feature detection
209
- license_type = determine_license_type()
210
- return {
211
- "Id" => "iDRAC-License",
212
- "Description" => "iDRAC8 #{license_type} License",
213
- "Name" => "iDRAC License",
214
- "LicenseType" => license_type,
215
- "Status" => { "Health" => "OK" },
216
- "Removable" => false
217
- }
218
- end
219
-
220
- # Handle response from Dell Manager entity
221
- def handle_dell_manager_response(data)
222
- # Look for license information in Oem data
223
- if data["Oem"] && data["Oem"]["Dell"]
224
- dell_data = data["Oem"]["Dell"]
225
-
226
- if dell_data["DellLicenseManagementService"]
227
- # Found license service reference, but need to query it directly
228
- service_uri = dell_data["DellLicenseManagementService"]["@odata.id"]
229
- debug "Found license service URI: #{service_uri}", 2
230
-
231
- service_response = authenticated_request(:get, service_uri)
232
- if service_response.status == 200
233
- return handle_dell_license_service_response(JSON.parse(service_response.body))
81
+
82
+ # Try manager entity OEM data
83
+ if (data = safe_get("/redfish/v1/Managers/iDRAC.Embedded.1"))
84
+ if (service_uri = data.dig("Oem", "Dell", "DellLicenseManagementService", "@odata.id"))
85
+ if (svc_data = safe_get(service_uri)) && svc_data["Members"]&.any?
86
+ if (details = safe_get(svc_data["Members"][0]["@odata.id"]))
87
+ return {
88
+ "Id" => details["EntitlementID"] || "iDRAC-License",
89
+ "Description" => details["LicenseDescription"] || "iDRAC License",
90
+ "Name" => details["LicenseDescription"] || "iDRAC License",
91
+ "LicenseType" => details["LicenseType"] || license_type_from(details["LicenseDescription"]),
92
+ "Status" => { "Health" => "OK" },
93
+ "Removable" => true
94
+ }
95
+ end
234
96
  end
235
97
  end
236
-
237
- # Check if license info is embedded directly
98
+
99
+ dell_data = data.dig("Oem", "Dell") || {}
238
100
  if dell_data["LicenseType"] || dell_data["License"]
239
- license_type = dell_data["LicenseType"] ||
240
- (dell_data["License"] && dell_data["License"].include?("Enterprise") ? "Enterprise" :
241
- dell_data["License"].include?("Express") ? "Express" : "Basic")
242
-
243
- return {
244
- "Id" => "iDRAC-License",
245
- "Description" => "iDRAC8 #{license_type} License",
246
- "Name" => "iDRAC License",
247
- "LicenseType" => license_type,
248
- "Status" => { "Health" => "OK" },
249
- "Removable" => false
250
- }
251
- end
252
- end
253
-
254
- # Check for license type in model name or description
255
- if data["Model"] && data["Model"].include?("Enterprise")
256
- return {
257
- "Id" => "iDRAC-License",
258
- "Description" => "iDRAC8 Enterprise License",
259
- "Name" => "iDRAC License",
260
- "LicenseType" => "Enterprise",
261
- "Status" => { "Health" => "OK" },
262
- "Removable" => false
263
- }
264
- end
265
-
266
- # If no license info found in manager data, fall back to feature detection
267
- license_type = determine_license_type()
268
- return {
269
- "Id" => "iDRAC-License",
270
- "Description" => "iDRAC8 #{license_type} License",
271
- "Name" => "iDRAC License",
272
- "LicenseType" => license_type,
273
- "Status" => { "Health" => "OK" },
274
- "Removable" => false
275
- }
276
- end
277
-
278
- # Get service tag from system info
279
- def get_service_tag
280
- response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
281
- if response.status == 200
282
- data = JSON.parse(response.body)
283
- return data["SKU"] if data["SKU"] # Service tag is usually in SKU field
284
- end
285
-
286
- # Try alternate location
287
- response = authenticated_request(:get, "/redfish/v1")
288
- if response.status == 200
289
- data = JSON.parse(response.body)
290
- if data["Oem"] && data["Oem"]["Dell"] && data["Oem"]["Dell"]["ServiceTag"]
291
- return data["Oem"]["Dell"]["ServiceTag"]
101
+ return build_license_hash(license_type_from(dell_data["LicenseType"] || dell_data["License"]))
292
102
  end
293
103
  end
294
-
295
- nil
104
+
105
+ # Last resort: detect from features
106
+ build_license_hash(detect_license_type)
296
107
  end
297
-
298
- # Helper method to extract license type from description
299
- def get_license_type_from_description(description)
300
- return "Unknown" unless description
301
-
302
- if description.include?("Enterprise")
303
- return "Enterprise"
304
- elsif description.include?("Express")
305
- return "Express"
306
- elsif description.include?("Datacenter")
307
- return "Datacenter"
308
- else
309
- return "Basic"
310
- end
108
+
109
+ def license_type_from(str)
110
+ return "Unknown" unless str
111
+ return "Enterprise" if str.include?("Enterprise")
112
+ return "Datacenter" if str.include?("Datacenter")
113
+ return "Express" if str.include?("Express")
114
+ "Basic"
311
115
  end
312
-
313
- # Creates a basic license info object based on system information
314
- # Used as a fallback when neither the standard nor Dell OEM endpoints work
315
- # @return [Hash] A basic license info object
316
- def create_fallback_license_info(use_basic: false)
317
- # Get the iDRAC version number from server headers
318
- version = nil
319
- response = authenticated_request(:get, "/redfish/v1")
320
- if response.headers["server"] && response.headers["server"].match(/iDRAC\/(\d+)/i)
321
- version = response.headers["server"].match(/iDRAC\/(\d+)/i)[1].to_i
322
- end
323
-
324
- # Try to determine if it's Enterprise or Express based on available features
325
- license_type = use_basic ? "Basic" : determine_license_type
326
-
327
- license_info = {
116
+
117
+ def build_license_hash(type)
118
+ {
328
119
  "Id" => "iDRAC-License",
329
- "Description" => version ? "iDRAC#{version} #{license_type} License" : "iDRAC License",
120
+ "Description" => "iDRAC #{type} License",
330
121
  "Name" => "iDRAC License",
331
- "LicenseType" => license_type,
122
+ "LicenseType" => type,
332
123
  "Status" => { "Health" => "OK" },
333
124
  "Removable" => false
334
125
  }
335
-
336
- debug "Created fallback license info: #{license_info}", 2
337
- license_info
338
126
  end
339
-
340
- # Attempt to determine the license type (Enterprise/Express) based on available features
341
- # @return [String] The license type (Enterprise, Express, or Basic)
342
- def determine_license_type
343
- # We can try to check for features only available in Enterprise
344
- begin
345
- # For example, virtual media is typically an Enterprise feature
346
- virtual_media_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia")
347
- # If we successfully get virtual media, it's likely Enterprise
348
- if virtual_media_response.status == 200
349
- return "Enterprise"
350
- end
351
- rescue
352
- # If the request fails, don't fail the whole method
353
- end
354
-
355
- # Default to basic license if we can't determine
356
- return "Express"
127
+
128
+ def detect_license_type
129
+ # Virtual media is typically Enterprise-only
130
+ data = safe_get("/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia")
131
+ data ? "Enterprise" : "Express"
357
132
  end
358
133
  end
359
- end
134
+ end
@@ -169,8 +169,7 @@ module IDRAC
169
169
  response = authenticated_request(
170
170
  :patch,
171
171
  "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellAttributes/LifecycleController.Embedded.1",
172
- body: payload.to_json,
173
- headers: { 'Content-Type': 'application/json' }
172
+ body: payload.to_json
174
173
  )
175
174
 
176
175
  code = response.status
@@ -210,10 +209,9 @@ module IDRAC
210
209
  payload = { "Component": ["LCData"] }
211
210
 
212
211
  response = authenticated_request(
213
- :post,
214
- path,
215
- body: payload.to_json,
216
- headers: { 'Content-Type' => 'application/json' }
212
+ :post,
213
+ path,
214
+ body: payload.to_json
217
215
  )
218
216
 
219
217
  if response.status.between?(200, 299)
@@ -264,7 +262,7 @@ module IDRAC
264
262
  def clear_system_event_logs!
265
263
  path = '/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Actions/LogService.ClearLog'
266
264
 
267
- response = authenticated_request(:post, path, body: {}.to_json, headers: { 'Content-Type' => 'application/json' })
265
+ response = authenticated_request(:post, path, body: {}.to_json)
268
266
 
269
267
  if response.status.between?(200, 299)
270
268
  debug "System Event Logs cleared", 0, :green
data/lib/idrac/network.rb CHANGED
@@ -171,8 +171,7 @@ module IDRAC
171
171
  response = authenticated_request(
172
172
  :patch,
173
173
  interface_path,
174
- body: body.to_json,
175
- headers: { 'Content-Type' => 'application/json' }
174
+ body: body.to_json
176
175
  )
177
176
  puts "✅ Got response with status: #{response.status}".green
178
177
  rescue => e
@@ -189,8 +188,7 @@ module IDRAC
189
188
  restart_response = authenticated_request(
190
189
  :post,
191
190
  "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset",
192
- body: { "ResetType" => "GracefulRestart" }.to_json,
193
- headers: { 'Content-Type' => 'application/json' }
191
+ body: { "ResetType" => "GracefulRestart" }.to_json
194
192
  )
195
193
 
196
194
  if restart_response.status == 204
data/lib/idrac/power.rb CHANGED
@@ -4,8 +4,7 @@ require 'colorize'
4
4
  module IDRAC
5
5
  module Power
6
6
  def power_on(wait: true)
7
- # Login to iDRAC if needed
8
- login unless @session_id
7
+ ensure_authenticated!
9
8
 
10
9
  puts "Powering on server...".light_cyan
11
10
 
@@ -22,7 +21,7 @@ module IDRAC
22
21
 
23
22
  tries = 10
24
23
  while tries > 0
25
- response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
24
+ response = authenticated_request(:post, path, body: payload.to_json)
26
25
 
27
26
  case response.status
28
27
  when 200, 204
@@ -60,8 +59,7 @@ module IDRAC
60
59
  end
61
60
 
62
61
  def power_off(wait: true, kind: "ForceOff")
63
- # Login to iDRAC if needed
64
- login unless @session_id
62
+ ensure_authenticated!
65
63
 
66
64
  puts "Powering off server...".light_cyan
67
65
 
@@ -76,12 +74,10 @@ module IDRAC
76
74
  path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
77
75
  payload = { "ResetType" => kind }
78
76
 
79
- response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
80
-
77
+ response = authenticated_request(:post, path, body: payload.to_json)
78
+
81
79
  if response.status == 409
82
80
  puts "Server is already powered OFF.".yellow
83
- else
84
- handle_response(response)
85
81
  end
86
82
 
87
83
  # Wait for power state change if requested
@@ -98,8 +94,7 @@ module IDRAC
98
94
  end
99
95
 
100
96
  def reboot
101
- # Login to iDRAC if needed
102
- login unless @session_id
97
+ ensure_authenticated!
103
98
 
104
99
  puts "Rebooting server...".light_cyan
105
100
 
@@ -114,7 +109,7 @@ module IDRAC
114
109
  path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
115
110
  payload = { "ResetType" => "ForceRestart" }
116
111
 
117
- response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
112
+ response = authenticated_request(:post, path, body: payload.to_json)
118
113
 
119
114
  if response.status >= 200 && response.status < 300
120
115
  puts "Server reboot command sent successfully".green
@@ -125,31 +120,27 @@ module IDRAC
125
120
  # Try gracefulRestart as an alternative
126
121
  puts "Trying GracefulRestart instead...".yellow
127
122
  payload = { "ResetType" => "GracefulRestart" }
128
- response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
129
-
130
- handle_response(response)
123
+ authenticated_request(:post, path, body: payload.to_json)
131
124
  else
132
125
  raise Error, "Failed to reboot server. Status code: #{response.status}"
133
126
  end
134
127
  end
135
128
 
136
129
  def get_power_state
137
- # Login to iDRAC if needed
138
- login unless @session_id
130
+ ensure_authenticated!
139
131
 
140
132
  # Get system information
141
133
  response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=PowerState")
142
-
143
- JSON.parse(handle_response(response))&.dig("PowerState")
134
+
135
+ JSON.parse(response.body)&.dig("PowerState")
144
136
  end
145
137
 
146
138
  def get_power_usage_watts
147
- # Login to iDRAC if needed
148
- login unless @session_id
139
+ ensure_authenticated!
149
140
 
150
141
  response = authenticated_request(:get, "/redfish/v1/Chassis/System.Embedded.1/Power")
151
-
152
- JSON.parse(handle_response(response))&.dig("PowerControl", 0, "PowerConsumedWatts")&.to_f
142
+
143
+ JSON.parse(response.body)&.dig("PowerControl", 0, "PowerConsumedWatts")&.to_f
153
144
  end
154
145
 
155
146
  # TODO: Migrate method names to match radfish interface for uniformity:
data/lib/idrac/session.rb CHANGED
@@ -180,38 +180,36 @@ module IDRAC
180
180
  debug "Skipping Redfish session creation (direct mode)", 1, :light_yellow
181
181
  return false
182
182
  end
183
-
184
- # Determine the correct session endpoint based on Redfish version
183
+
184
+ # Skip if already have a valid token
185
+ if @x_auth_token
186
+ debug "Session already active", 1, :green
187
+ return true
188
+ end
189
+
190
+ # Determine the correct session endpoint (cached after first call)
185
191
  session_endpoint = determine_session_endpoint
186
-
187
- # Check current session count before creating new session
188
- current_sessions = get_session_count
189
- debug "Current active sessions: #{current_sessions}", 1, :cyan
190
-
192
+
191
193
  payload = { "UserName" => username, "Password" => password }
192
-
194
+
193
195
  debug "Attempting to create Redfish session at #{base_url}#{session_endpoint}", 1
194
196
  debug "SSL verification: #{verify_ssl ? 'Enabled' : 'Disabled'}", 1
195
197
  print_connection_debug_info if @verbosity >= 2
196
-
198
+
197
199
  # Try creation methods in sequence
198
200
  if create_session_with_content_type(session_endpoint, payload)
199
- log_session_creation_success(current_sessions)
200
201
  return true
201
202
  end
202
203
  if create_session_with_basic_auth(session_endpoint, payload)
203
- log_session_creation_success(current_sessions)
204
204
  return true
205
205
  end
206
206
  if handle_max_sessions_and_retry(session_endpoint, payload)
207
- log_session_creation_success(current_sessions)
208
207
  return true
209
208
  end
210
209
  if create_session_with_form_urlencoded(session_endpoint, payload)
211
- log_session_creation_success(current_sessions)
212
210
  return true
213
211
  end
214
-
212
+
215
213
  # If all attempts fail, switch to direct mode
216
214
  @direct_mode = true
217
215
  false
@@ -652,32 +650,32 @@ module IDRAC
652
650
  end
653
651
  end
654
652
 
655
- # Determine the correct session endpoint based on Redfish version
653
+ # Determine the correct session endpoint based on Redfish version (cached)
656
654
  def determine_session_endpoint
655
+ return @session_endpoint if @session_endpoint
656
+
657
657
  begin
658
658
  debug "Checking Redfish version to determine session endpoint...", 1
659
-
659
+
660
660
  response = connection.get('/redfish/v1') do |req|
661
661
  req.headers['Accept'] = 'application/json'
662
662
  req.headers['Host'] = host_header if host_header
663
663
  end
664
-
664
+
665
665
  if response.status == 200
666
666
  begin
667
667
  data = JSON.parse(response.body)
668
668
  redfish_version = data['RedfishVersion']
669
-
669
+
670
670
  if redfish_version
671
671
  debug "Detected Redfish version: #{redfish_version}", 1
672
-
673
- # For version 1.17.0 and below, use the /redfish/v1/Sessions endpoint
674
- # For newer versions, use /redfish/v1/SessionService/Sessions
675
- endpoint = Gem::Version.new(redfish_version) <= Gem::Version.new('1.17.0') ?
676
- '/redfish/v1/Sessions' :
672
+
673
+ @session_endpoint = Gem::Version.new(redfish_version) <= Gem::Version.new('1.17.0') ?
674
+ '/redfish/v1/Sessions' :
677
675
  '/redfish/v1/SessionService/Sessions'
678
-
679
- debug "Using endpoint #{endpoint} for Redfish version #{redfish_version}", 1
680
- return endpoint
676
+
677
+ debug "Using endpoint #{@session_endpoint} for Redfish version #{redfish_version}", 1
678
+ return @session_endpoint
681
679
  end
682
680
  rescue JSON::ParserError => e
683
681
  debug "Error parsing Redfish version: #{e.message}", 1, :red
@@ -688,11 +686,10 @@ module IDRAC
688
686
  rescue => e
689
687
  debug "Error checking Redfish version: #{e.message}", 1, :red
690
688
  end
691
-
692
- # Default to /redfish/v1/Sessions if we can't determine version
693
- default_endpoint = '/redfish/v1/Sessions'
694
- debug "Defaulting to endpoint #{default_endpoint}", 1, :light_yellow
695
- default_endpoint
689
+
690
+ @session_endpoint = '/redfish/v1/Sessions'
691
+ debug "Defaulting to endpoint #{@session_endpoint}", 1, :light_yellow
692
+ @session_endpoint
696
693
  end
697
694
 
698
695
  # Get current session count
data/lib/idrac/storage.rb CHANGED
@@ -361,8 +361,6 @@ module IDRAC
361
361
  puts "Deleting volume: #{path}"
362
362
 
363
363
  response = authenticated_request(:delete, "/redfish/v1/#{path}")
364
-
365
- handle_response(response)
366
364
  end
367
365
 
368
366
  # Create a new virtual disk with RAID5 and FastPath optimizations
@@ -430,13 +428,10 @@ module IDRAC
430
428
  end
431
429
 
432
430
  response = authenticated_request(
433
- :post,
431
+ :post,
434
432
  "#{controller_path}/Volumes",
435
- body: payload.to_json,
436
- headers: { 'Content-Type' => 'application/json' }
433
+ body: payload.to_json
437
434
  )
438
-
439
- handle_response(response)
440
435
  end
441
436
 
442
437
 
@@ -541,24 +536,20 @@ module IDRAC
541
536
  response = authenticated_request(
542
537
  :post,
543
538
  "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.SetControllerKey",
544
- body: payload.to_json,
545
- headers: { 'Content-Type': 'application/json' }
539
+ body: payload.to_json
546
540
  )
547
541
 
548
542
  if response.status == 202
549
543
  puts "Controller encryption enabled".green
550
-
544
+
551
545
  # Check if we need to wait for a job
552
546
  if response.headers["location"]
553
547
  job_id = response.headers["location"].split("/").last
554
548
  wait_for_job(job_id)
555
549
  end
556
-
557
- return true
558
- else
559
- # Use generic error handler which includes ExtendedInfo parsing
560
- handle_response(response)
561
550
  end
551
+
552
+ true
562
553
  end
563
554
 
564
555
  # Disable Self-Encrypting Drive support on controller
@@ -568,24 +559,20 @@ module IDRAC
568
559
  response = authenticated_request(
569
560
  :post,
570
561
  "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.RemoveControllerKey",
571
- body: payload.to_json,
572
- headers: { 'Content-Type': 'application/json' }
562
+ body: payload.to_json
573
563
  )
574
564
 
575
565
  if response.status == 202
576
566
  puts "Controller encryption disabled".green
577
-
567
+
578
568
  # Check if we need to wait for a job
579
569
  if response.headers["location"]
580
570
  job_id = response.headers["location"].split("/").last
581
571
  wait_for_job(job_id)
582
572
  end
583
-
584
- return true
585
- else
586
- # Use generic error handler which includes ExtendedInfo parsing
587
- handle_response(response)
588
573
  end
574
+
575
+ true
589
576
  end
590
577
 
591
578
  # Check if all physical disks are Self-Encrypting Drives
data/lib/idrac/system.rb CHANGED
@@ -708,10 +708,9 @@ module IDRAC
708
708
  # Clear system event logs
709
709
  def clear_system_event_logs
710
710
  response = authenticated_request(
711
- :post,
711
+ :post,
712
712
  "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Actions/LogService.ClearLog",
713
- body: {}.to_json,
714
- headers: { 'Content-Type': 'application/json' }
713
+ body: {}.to_json
715
714
  )
716
715
 
717
716
  if response.status.between?(200, 299)
@@ -39,10 +39,9 @@ module IDRAC
39
39
 
40
40
  # Submit configuration with job availability handling
41
41
  res = wait_for_job_availability do
42
- authenticated_request(:post,
42
+ authenticated_request(:post,
43
43
  "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
44
- body: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}}.to_json,
45
- headers: {"Content-Type" => "application/json"}
44
+ body: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}}.to_json
46
45
  )
47
46
  end
48
47
 
@@ -58,10 +57,9 @@ module IDRAC
58
57
  # Get the system configuration profile for a given target (e.g. "RAID")
59
58
  def get_system_configuration_profile(target: "RAID")
60
59
  debug "Exporting System Configuration..."
61
- response = authenticated_request(:post,
62
- "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration",
63
- body: {"ExportFormat": "JSON", "ShareParameters":{"Target": target}}.to_json,
64
- headers: {"Content-Type" => "application/json"}
60
+ response = authenticated_request(:post,
61
+ "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration",
62
+ body: {"ExportFormat": "JSON", "ShareParameters":{"Target": target}}.to_json
65
63
  )
66
64
  scp = handle_location(response.headers["location"])
67
65
  # We experienced this with older iDRACs, so let's give a enriched error to help debug.
@@ -117,10 +115,9 @@ module IDRAC
117
115
 
118
116
  # Make the API request
119
117
  response = authenticated_request(
120
- :post,
118
+ :post,
121
119
  "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
122
- body: params.to_json,
123
- headers: {"Content-Type" => "application/json"}
120
+ body: params.to_json
124
121
  )
125
122
 
126
123
  # Check for immediate errors
data/lib/idrac/utility.rb CHANGED
@@ -61,8 +61,7 @@ module IDRAC
61
61
  response = authenticated_request(
62
62
  :post,
63
63
  "/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.SupportAssistCollection",
64
- body: payload.to_json,
65
- headers: { 'Content-Type' => 'application/json' }
64
+ body: payload.to_json
66
65
  )
67
66
 
68
67
  case response.status
@@ -303,8 +302,7 @@ module IDRAC
303
302
  response = authenticated_request(
304
303
  :post,
305
304
  "/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.SupportAssistGetEULAStatus",
306
- body: {}.to_json,
307
- headers: { 'Content-Type' => 'application/json' }
305
+ body: {}.to_json
308
306
  )
309
307
 
310
308
  if response.status.between?(200, 299)
@@ -333,8 +331,7 @@ module IDRAC
333
331
  response = authenticated_request(
334
332
  :post,
335
333
  "/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.SupportAssistAcceptEULA",
336
- body: {}.to_json,
337
- headers: { 'Content-Type' => 'application/json' }
334
+ body: {}.to_json
338
335
  )
339
336
 
340
337
  if response.status.between?(200, 299)
@@ -357,8 +354,7 @@ module IDRAC
357
354
  response = authenticated_request(
358
355
  :post,
359
356
  "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset",
360
- body: { "ResetType" => "GracefulRestart" }.to_json,
361
- headers: { 'Content-Type' => 'application/json' }
357
+ body: { "ResetType" => "GracefulRestart" }.to_json
362
358
  )
363
359
 
364
360
  if response.status.between?(200, 299)
data/lib/idrac/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.4"
5
5
  end
@@ -71,13 +71,11 @@ module IDRAC
71
71
  end
72
72
 
73
73
  response = authenticated_request(
74
- :post,
74
+ :post,
75
75
  "/redfish/v1/#{path}",
76
- body: {}.to_json,
77
- headers: { 'Content-Type': 'application/json' }
76
+ body: {}.to_json
78
77
  )
79
78
 
80
- handle_response(response)
81
79
  response.status.between?(200, 299)
82
80
  end
83
81
 
@@ -106,10 +104,9 @@ module IDRAC
106
104
  end
107
105
 
108
106
  response = authenticated_request(
109
- :post,
107
+ :post,
110
108
  "/redfish/v1/#{path}",
111
- body: { "Image": iso_url, "Inserted": true, "WriteProtected": true }.to_json,
112
- headers: { 'Content-Type': 'application/json' }
109
+ body: { "Image": iso_url, "Inserted": true, "WriteProtected": true }.to_json
113
110
  )
114
111
 
115
112
  if response.status == 204 || response.status == 200
@@ -158,10 +155,9 @@ module IDRAC
158
155
 
159
156
  # Set one-time boot to CD
160
157
  response = authenticated_request(
161
- :patch,
158
+ :patch,
162
159
  "/redfish/v1/Systems/System.Embedded.1",
163
- body: { "Boot": { "BootSourceOverrideTarget": "Cd", "BootSourceOverrideEnabled": "Once" } }.to_json,
164
- headers: { 'Content-Type': 'application/json' }
160
+ body: { "Boot": { "BootSourceOverrideTarget": "Cd", "BootSourceOverrideEnabled": "Once" } }.to_json
165
161
  )
166
162
 
167
163
  if response.status.between?(200, 299)
@@ -189,10 +185,9 @@ module IDRAC
189
185
  }
190
186
 
191
187
  response = authenticated_request(
192
- :patch,
188
+ :patch,
193
189
  "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes",
194
- body: payload.to_json,
195
- headers: { 'Content-Type': 'application/json' }
190
+ body: payload.to_json
196
191
  )
197
192
 
198
193
  if response.status.between?(200, 299)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-10-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: httparty
@@ -282,7 +281,6 @@ licenses:
282
281
  metadata:
283
282
  homepage_uri: https://github.com/buildio/idrac
284
283
  source_code_uri: https://github.com/buildio/idrac
285
- post_install_message:
286
284
  rdoc_options: []
287
285
  require_paths:
288
286
  - lib
@@ -297,8 +295,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
297
295
  - !ruby/object:Gem::Version
298
296
  version: '0'
299
297
  requirements: []
300
- rubygems_version: 3.5.16
301
- signing_key:
298
+ rubygems_version: 3.6.9
302
299
  specification_version: 4
303
300
  summary: API Client for Dell iDRAC
304
301
  test_files: []