idrac 0.1.92 → 0.3.1

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.
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add ActiveSupport-like blank? method to core Ruby classes
4
+ # This allows us to use blank? without requiring Rails' ActiveSupport
5
+
6
+ class NilClass
7
+ # nil is always blank
8
+ def blank?
9
+ true
10
+ end
11
+ end
12
+
13
+ class String
14
+ # A string is blank if it's empty or contains whitespace only
15
+ def blank?
16
+ strip.empty?
17
+ end
18
+ end
19
+
20
+ class Array
21
+ # An array is blank if it's empty
22
+ def blank?
23
+ empty?
24
+ end
25
+ end
26
+
27
+ class Hash
28
+ # A hash is blank if it's empty
29
+ def blank?
30
+ empty?
31
+ end
32
+ end
33
+
34
+ class Object
35
+ # An object is blank if it responds to empty? and is empty
36
+ # Otherwise return false
37
+ def blank?
38
+ respond_to?(:empty?) ? empty? : false
39
+ end
40
+ end
41
+
42
+ # Add ActiveSupport-like numeric extensions
43
+ class Integer
44
+ # Byte size helpers
45
+ def byte
46
+ self
47
+ end
48
+ alias_method :bytes, :byte
49
+
50
+ def kilobyte
51
+ self * 1024
52
+ end
53
+ alias_method :kilobytes, :kilobyte
54
+
55
+ def megabyte
56
+ self * 1024 * 1024
57
+ end
58
+ alias_method :megabytes, :megabyte
59
+
60
+ def gigabyte
61
+ self * 1024 * 1024 * 1024
62
+ end
63
+ alias_method :gigabytes, :gigabyte
64
+
65
+ def terabyte
66
+ self * 1024 * 1024 * 1024 * 1024
67
+ end
68
+ alias_method :terabytes, :terabyte
69
+
70
+ def petabyte
71
+ self * 1024 * 1024 * 1024 * 1024 * 1024
72
+ end
73
+ alias_method :petabytes, :petabyte
74
+
75
+ # Time duration helpers (for potential future use)
76
+ def second
77
+ self
78
+ end
79
+ alias_method :seconds, :second
80
+
81
+ def minute
82
+ self * 60
83
+ end
84
+ alias_method :minutes, :minute
85
+
86
+ def hour
87
+ self * 60 * 60
88
+ end
89
+ alias_method :hours, :hour
90
+
91
+ def day
92
+ self * 24 * 60 * 60
93
+ end
94
+ alias_method :days, :day
95
+
96
+ def week
97
+ self * 7 * 24 * 60 * 60
98
+ end
99
+ alias_method :weeks, :week
100
+ end
data/lib/idrac/jobs.rb CHANGED
@@ -46,8 +46,6 @@ module IDRAC
46
46
  end
47
47
 
48
48
  # Clear all jobs from the job queue
49
- # Apparently too many jobs will slow down the iDRAC. Let's clear them out.
50
- # https://github.com/dell/iDRAC-Redfish-Scripting/issues/116
51
49
  def clear_jobs!
52
50
  # Get list of jobs
53
51
  jobs_response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)')
@@ -80,8 +78,6 @@ module IDRAC
80
78
  # Force clear the job queue
81
79
  def force_clear_jobs!
82
80
  # Clear the job queue using force option which will also clear any pending data and restart processes
83
- # Once you executed the command to force clear the job queue, wait a few minutes and then execute command
84
- # below to check LC status. Continue to execute this command until you see LC status reported as Ready which shouldn't take longer than a couple of minutes.
85
81
  path = '/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellJobService/Actions/DellJobService.DeleteJobQueue'
86
82
  payload = { "JobID" => "JID_CLEARALL_FORCE" }
87
83
 
data/lib/idrac/session.rb CHANGED
@@ -203,29 +203,78 @@ module IDRAC
203
203
 
204
204
  # Delete the Redfish session
205
205
  def delete
206
- return unless @x_auth_token && @session_location
206
+ return false unless @x_auth_token || @session_location
207
207
 
208
208
  begin
209
209
  debug "Deleting Redfish session...", 1
210
210
 
211
- # Use the X-Auth-Token for authentication
212
- headers = { 'X-Auth-Token' => @x_auth_token }
213
-
214
- response = connection.delete(@session_location) do |req|
215
- req.headers.merge!(headers)
211
+ if @session_location
212
+ # Use the X-Auth-Token for authentication
213
+ headers = { 'X-Auth-Token' => @x_auth_token }
214
+
215
+ begin
216
+ response = connection.delete(@session_location) do |req|
217
+ req.headers.merge!(headers)
218
+ end
219
+
220
+ if response.status == 200 || response.status == 204
221
+ debug "Redfish session deleted successfully", 1, :green
222
+ @x_auth_token = nil
223
+ @session_location = nil
224
+ return true
225
+ end
226
+ rescue => session_e
227
+ debug "Error during session deletion via location: #{session_e.message}", 1, :yellow
228
+ # Continue to try basic auth method
229
+ end
216
230
  end
217
231
 
218
- if response.status == 200 || response.status == 204
219
- debug "Redfish session deleted successfully", 1, :green
232
+ # If deleting via session location fails or there's no session location,
233
+ # try to delete by using the basic auth method
234
+ if @x_auth_token
235
+ # Try to determine session ID from the X-Auth-Token or session_location
236
+ session_id = nil
237
+
238
+ # Extract session ID from location if available
239
+ if @session_location
240
+ if @session_location =~ /\/([^\/]+)$/
241
+ session_id = $1
242
+ end
243
+ end
244
+
245
+ # If we have an extracted session ID
246
+ if session_id
247
+ debug "Trying to delete session by ID #{session_id}", 1
248
+
249
+ begin
250
+ endpoint = determine_session_endpoint
251
+ delete_url = "#{endpoint}/#{session_id}"
252
+
253
+ delete_response = request_with_basic_auth(:delete, delete_url, nil)
254
+
255
+ if delete_response.status == 200 || delete_response.status == 204
256
+ debug "Successfully deleted session via ID", 1, :green
257
+ @x_auth_token = nil
258
+ @session_location = nil
259
+ return true
260
+ end
261
+ rescue => id_e
262
+ debug "Error during session deletion via ID: #{id_e.message}", 1, :yellow
263
+ end
264
+ end
265
+
266
+ # Last resort: clear the token variable even if we couldn't properly delete it
267
+ debug "Clearing session token internally", 1, :yellow
220
268
  @x_auth_token = nil
221
269
  @session_location = nil
222
- return true
223
- else
224
- debug "Failed to delete Redfish session: #{response.status} - #{response.body}", 1, :red
225
- return false
226
270
  end
271
+
272
+ return false
227
273
  rescue => e
228
274
  debug "Error during Redfish session deletion: #{e.message}", 1, :red
275
+ # Clear token variable anyway
276
+ @x_auth_token = nil
277
+ @session_location = nil
229
278
  return false
230
279
  end
231
280
  end
@@ -0,0 +1,374 @@
1
+ require 'json'
2
+ require 'colorize'
3
+
4
+ module IDRAC
5
+ module StorageMethods
6
+ # Get storage controllers information
7
+ def controller
8
+ response = authenticated_request(:get, '/redfish/v1/Systems/System.Embedded.1/Storage?$expand=*($levels=1)')
9
+
10
+ if response.status == 200
11
+ begin
12
+ data = JSON.parse(response.body)
13
+
14
+ puts "Controllers".green
15
+ data["Members"].each { |ctrlr| puts "#{ctrlr["Name"]} > #{ctrlr["Drives@odata.count"]}" }
16
+
17
+ puts "Drives".green
18
+ data["Members"].each do |m|
19
+ puts "Storage: #{m["Name"]} > #{m["Status"]["Health"] || "N/A"} > #{m["Drives"].size}"
20
+ end
21
+
22
+ # Find the controller with the most drives (usually the PERC)
23
+ controller = data["Members"].sort_by { |ctrlr| ctrlr["Drives"].size }.last
24
+
25
+ if controller["Name"] =~ /PERC/
26
+ puts "Found #{controller["Name"]}".green
27
+ else
28
+ puts "Found #{controller["Name"]} but continuing...".yellow
29
+ end
30
+
31
+ return controller
32
+ rescue JSON::ParserError
33
+ raise Error, "Failed to parse controller response: #{response.body}"
34
+ end
35
+ else
36
+ raise Error, "Failed to get controller. Status code: #{response.status}"
37
+ end
38
+ end
39
+
40
+ # Check if controller supports encryption
41
+ def controller_encryption_capable?(controller)
42
+ return false unless controller
43
+ controller.dig("Oem", "Dell", "DellController", "EncryptionCapability") =~ /localkey/i
44
+ end
45
+
46
+ # Check if controller encryption is enabled
47
+ def controller_encryption_enabled?(controller)
48
+ return false unless controller
49
+ controller.dig("Oem", "Dell", "DellController", "EncryptionMode") =~ /localkey/i
50
+ end
51
+
52
+ # Get information about physical drives
53
+ def drives(controller)
54
+ raise Error, "Controller not provided" unless controller
55
+
56
+ controller_path = controller["@odata.id"].split("v1/").last
57
+ response = authenticated_request(:get, "/redfish/v1/#{controller_path}?$expand=*($levels=1)")
58
+
59
+ if response.status == 200
60
+ begin
61
+ data = JSON.parse(response.body)
62
+ drives = data["Drives"].map do |body|
63
+ serial = body["SerialNumber"]
64
+ serial = body["Identifiers"].first["DurableName"] if serial.blank?
65
+ {
66
+ serial: serial,
67
+ model: body["Model"],
68
+ name: body["Name"],
69
+ capacity_bytes: body["CapacityBytes"],
70
+ health: body["Status"]["Health"] ? body["Status"]["Health"] : "N/A",
71
+ speed_gbp: body["CapableSpeedGbs"],
72
+ manufacturer: body["Manufacturer"],
73
+ media_type: body["MediaType"],
74
+ failure_predicted: body["FailurePredicted"],
75
+ life_left_percent: body["PredictedMediaLifeLeftPercent"],
76
+ certified: body.dig("Oem", "Dell", "DellPhysicalDisk", "Certified"),
77
+ raid_status: body.dig("Oem", "Dell", "DellPhysicalDisk", "RaidStatus"),
78
+ operation_name: body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationName"),
79
+ operation_progress: body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationPercentCompletePercent"),
80
+ encryption_ability: body["EncryptionAbility"],
81
+ "@odata.id": body["@odata.id"]
82
+ }
83
+ end
84
+
85
+ return drives.sort_by { |d| d[:name] }
86
+ rescue JSON::ParserError
87
+ raise Error, "Failed to parse drives response: #{response.body}"
88
+ end
89
+ else
90
+ raise Error, "Failed to get drives. Status code: #{response.status}"
91
+ end
92
+ end
93
+
94
+ # Get information about virtual disk volumes
95
+ def volumes(controller)
96
+ raise Error, "Controller not provided" unless controller
97
+
98
+ puts "Volumes (e.g. Arrays)".green
99
+
100
+ v = controller["Volumes"]
101
+ path = v["@odata.id"].split("v1/").last
102
+ response = authenticated_request(:get, "/redfish/v1/#{path}?$expand=*($levels=1)")
103
+
104
+ if response.status == 200
105
+ begin
106
+ data = JSON.parse(response.body)
107
+ volumes = data["Members"].map do |vol|
108
+ drives = vol["Links"]["Drives"]
109
+ volume = {
110
+ name: vol["Name"],
111
+ capacity_bytes: vol["CapacityBytes"],
112
+ volume_type: vol["VolumeType"],
113
+ drives: drives,
114
+ write_cache_policy: vol.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy"),
115
+ read_cache_policy: vol.dig("Oem", "Dell", "DellVirtualDisk", "ReadCachePolicy"),
116
+ stripe_size: vol.dig("Oem", "Dell", "DellVirtualDisk", "StripeSize"),
117
+ raid_level: vol["RAIDType"],
118
+ encrypted: vol["Encrypted"],
119
+ lock_status: vol.dig("Oem", "Dell", "DellVirtualDisk", "LockStatus"),
120
+ "@odata.id": vol["@odata.id"]
121
+ }
122
+
123
+ # Check FastPath settings
124
+ volume[:fastpath] = fastpath_good?(volume)
125
+
126
+ # Handle volume operations and status
127
+ if vol["Operations"].any?
128
+ volume[:health] = vol["Status"]["Health"] ? vol["Status"]["Health"] : "N/A"
129
+ volume[:progress] = vol["Operations"].first["PercentageComplete"]
130
+ volume[:message] = vol["Operations"].first["OperationName"]
131
+ elsif vol["Status"]["Health"] == "OK"
132
+ volume[:health] = "OK"
133
+ volume[:progress] = nil
134
+ volume[:message] = nil
135
+ else
136
+ volume[:health] = "?"
137
+ volume[:progress] = nil
138
+ volume[:message] = nil
139
+ end
140
+
141
+ volume
142
+ end
143
+
144
+ return volumes.sort_by { |d| d[:name] }
145
+ rescue JSON::ParserError
146
+ raise Error, "Failed to parse volumes response: #{response.body}"
147
+ end
148
+ else
149
+ raise Error, "Failed to get volumes. Status code: #{response.status}"
150
+ end
151
+ end
152
+
153
+ # Check if FastPath is properly configured for a volume
154
+ def fastpath_good?(volume)
155
+ return "disabled" unless volume
156
+
157
+ # Modern firmware check handled by caller
158
+ if volume[:write_cache_policy] == "WriteThrough" &&
159
+ volume[:read_cache_policy] == "NoReadAhead" &&
160
+ volume[:stripe_size] == "64KB"
161
+ return "enabled"
162
+ else
163
+ return "disabled"
164
+ end
165
+ end
166
+
167
+ # Delete a volume
168
+ def delete_volume(odata_id)
169
+ path = odata_id.split("v1/").last
170
+ puts "Deleting volume: #{path}"
171
+
172
+ response = authenticated_request(:delete, "/redfish/v1/#{path}")
173
+
174
+ if response.status.between?(200, 299)
175
+ puts "Delete volume request sent".green
176
+
177
+ # Check if we need to wait for a job
178
+ if response.headers["location"]
179
+ job_id = response.headers["location"].split("/").last
180
+ wait_for_job(job_id)
181
+ end
182
+
183
+ return true
184
+ else
185
+ error_message = "Failed to delete volume. Status code: #{response.status}"
186
+
187
+ begin
188
+ error_data = JSON.parse(response.body)
189
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
190
+ rescue
191
+ # Ignore JSON parsing errors
192
+ end
193
+
194
+ raise Error, error_message
195
+ end
196
+ end
197
+
198
+ # Create a new virtual disk with RAID5 and FastPath optimizations
199
+ def create_virtual_disk(controller_id, drives, name: "vssd0", raid_type: "RAID5")
200
+ # Check firmware version to determine which API to use
201
+ firmware_version = get_firmware_version.split(".")[0,2].join.to_i
202
+
203
+ # [FastPath optimization for SSDs](https://www.dell.com/support/manuals/en-us/perc-h755/perc11_ug/fastpath?guid=guid-a9e90946-a41f-48ab-88f1-9ce514b4c414&lang=en-us)
204
+ payload = {
205
+ "Drives": drives.map { |d| { "@odata.id": d["@odata.id"] } },
206
+ "Name": name,
207
+ "OptimumIOSizeBytes": 64 * 1024,
208
+ "Oem": { "Dell": { "DellVolume": { "DiskCachePolicy": "Enabled" } } },
209
+ "ReadCachePolicy": "Off", # "NoReadAhead"
210
+ "WriteCachePolicy": "WriteThrough"
211
+ }
212
+
213
+ # If the firmware < 440, we need a different approach
214
+ if firmware_version >= 440
215
+ # For modern firmware
216
+ if drives.size < 3 && raid_type == "RAID5"
217
+ puts "*************************************************".red
218
+ puts "* WARNING: Less than 3 drives. Selecting RAID0. *".red
219
+ puts "*************************************************".red
220
+ payload["RAIDType"] = "RAID0"
221
+ else
222
+ payload["RAIDType"] = raid_type
223
+ end
224
+ else
225
+ # For older firmware
226
+ payload["VolumeType"] = "StripedWithParity" if raid_type == "RAID5"
227
+ payload["VolumeType"] = "SpannedDisks" if raid_type == "RAID0"
228
+ end
229
+
230
+ url = "Systems/System.Embedded.1/Storage/#{controller_id}/Volumes"
231
+ response = authenticated_request(
232
+ :post,
233
+ "/redfish/v1/#{url}",
234
+ body: payload.to_json,
235
+ headers: { 'Content-Type': 'application/json' }
236
+ )
237
+
238
+ if response.status.between?(200, 299)
239
+ puts "Virtual disk creation started".green
240
+
241
+ # Check if we need to wait for a job
242
+ if response.headers["location"]
243
+ job_id = response.headers["location"].split("/").last
244
+ wait_for_job(job_id)
245
+ end
246
+
247
+ return true
248
+ else
249
+ error_message = "Failed to create virtual disk. Status code: #{response.status}"
250
+
251
+ begin
252
+ error_data = JSON.parse(response.body)
253
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
254
+ rescue
255
+ # Ignore JSON parsing errors
256
+ end
257
+
258
+ raise Error, error_message
259
+ end
260
+ end
261
+
262
+ # Enable Self-Encrypting Drive support on controller
263
+ def enable_local_key_management(controller_id, passphrase: "Secure123!", keyid: "RAID-Key-2023")
264
+ payload = {
265
+ "TargetFQDD": controller_id,
266
+ "Key": passphrase,
267
+ "Keyid": keyid
268
+ }
269
+
270
+ response = authenticated_request(
271
+ :post,
272
+ "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.SetControllerKey",
273
+ body: payload.to_json,
274
+ headers: { 'Content-Type': 'application/json' }
275
+ )
276
+
277
+ if response.status == 202
278
+ puts "Controller encryption enabled".green
279
+
280
+ # Check if we need to wait for a job
281
+ if response.headers["location"]
282
+ job_id = response.headers["location"].split("/").last
283
+ wait_for_job(job_id)
284
+ end
285
+
286
+ return true
287
+ else
288
+ error_message = "Failed to enable controller encryption. Status code: #{response.status}"
289
+
290
+ begin
291
+ error_data = JSON.parse(response.body)
292
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
293
+ rescue
294
+ # Ignore JSON parsing errors
295
+ end
296
+
297
+ raise Error, error_message
298
+ end
299
+ end
300
+
301
+ # Disable Self-Encrypting Drive support on controller
302
+ def disable_local_key_management(controller_id)
303
+ payload = { "TargetFQDD": controller_id }
304
+
305
+ response = authenticated_request(
306
+ :post,
307
+ "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.RemoveControllerKey",
308
+ body: payload.to_json,
309
+ headers: { 'Content-Type': 'application/json' }
310
+ )
311
+
312
+ if response.status == 202
313
+ puts "Controller encryption disabled".green
314
+
315
+ # Check if we need to wait for a job
316
+ if response.headers["location"]
317
+ job_id = response.headers["location"].split("/").last
318
+ wait_for_job(job_id)
319
+ end
320
+
321
+ return true
322
+ else
323
+ error_message = "Failed to disable controller encryption. Status code: #{response.status}"
324
+
325
+ begin
326
+ error_data = JSON.parse(response.body)
327
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
328
+ rescue
329
+ # Ignore JSON parsing errors
330
+ end
331
+
332
+ raise Error, error_message
333
+ end
334
+ end
335
+
336
+ # Check if all physical disks are Self-Encrypting Drives
337
+ def all_seds?(drives)
338
+ drives.all? { |d| d[:encryption_ability] == "SelfEncryptingDrive" }
339
+ end
340
+
341
+ # Check if the system is ready for SED operations
342
+ def sed_ready?(controller, drives)
343
+ all_seds?(drives) && controller_encryption_capable?(controller) && controller_encryption_enabled?(controller)
344
+ end
345
+
346
+ # Get firmware version
347
+ def get_firmware_version
348
+ response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=FirmwareVersion")
349
+
350
+ if response.status == 200
351
+ begin
352
+ data = JSON.parse(response.body)
353
+ return data["FirmwareVersion"]
354
+ rescue JSON::ParserError
355
+ raise Error, "Failed to parse firmware version response: #{response.body}"
356
+ end
357
+ else
358
+ # Try again without the $select parameter for older firmware
359
+ response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1")
360
+
361
+ if response.status == 200
362
+ begin
363
+ data = JSON.parse(response.body)
364
+ return data["FirmwareVersion"]
365
+ rescue JSON::ParserError
366
+ raise Error, "Failed to parse firmware version response: #{response.body}"
367
+ end
368
+ else
369
+ raise Error, "Failed to get firmware version. Status code: #{response.status}"
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end