idrac 0.1.92 → 0.3.2

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,399 @@
1
+ require 'json'
2
+ require 'colorize'
3
+
4
+ module IDRAC
5
+ module StorageMethods
6
+ # Get storage controllers information
7
+ def controller
8
+ # Use the controllers method to get all controllers
9
+ controller_list = controllers
10
+
11
+ puts "Controllers".green
12
+ controller_list.each { |c| puts "#{c[:name]} > #{c[:drives_count]}" }
13
+
14
+ puts "Drives".green
15
+ controller_list.each do |c|
16
+ puts "Storage: #{c[:name]} > #{c[:status]} > #{c[:drives_count]}"
17
+ end
18
+
19
+ # Find the controller with the most drives (usually the PERC)
20
+ controller_info = controller_list.max_by { |c| c[:drives_count] }
21
+
22
+ if controller_info[:name] =~ /PERC/
23
+ puts "Found #{controller_info[:name]}".green
24
+ else
25
+ puts "Found #{controller_info[:name]} but continuing...".yellow
26
+ end
27
+
28
+ # Return the raw controller data
29
+ controller_info[:raw]
30
+ end
31
+
32
+ # Get all storage controllers and return them as an array
33
+ def controllers
34
+ response = authenticated_request(:get, '/redfish/v1/Systems/System.Embedded.1/Storage?$expand=*($levels=1)')
35
+
36
+ if response.status == 200
37
+ begin
38
+ data = JSON.parse(response.body)
39
+
40
+ # Transform and return all controllers as an array of hashes with consistent keys
41
+ controllers = data["Members"].map do |controller|
42
+ {
43
+ name: controller["Name"],
44
+ model: controller["Model"],
45
+ drives_count: controller["Drives"].size,
46
+ status: controller["Status"]["Health"] || "N/A",
47
+ firmware_version: controller.dig("StorageControllers", 0, "FirmwareVersion"),
48
+ encryption_mode: controller.dig("Oem", "Dell", "DellController", "EncryptionMode"),
49
+ encryption_capability: controller.dig("Oem", "Dell", "DellController", "EncryptionCapability"),
50
+ controller_type: controller.dig("Oem", "Dell", "DellController", "ControllerType"),
51
+ pci_slot: controller.dig("Oem", "Dell", "DellController", "PCISlot"),
52
+ raw: controller
53
+ }
54
+ end
55
+
56
+ return controllers.sort_by { |c| c[:name] }
57
+ rescue JSON::ParserError
58
+ raise Error, "Failed to parse controllers response: #{response.body}"
59
+ end
60
+ else
61
+ raise Error, "Failed to get controllers. Status code: #{response.status}"
62
+ end
63
+ end
64
+
65
+ # Check if controller supports encryption
66
+ def controller_encryption_capable?(controller)
67
+ return false unless controller
68
+ controller.dig("Oem", "Dell", "DellController", "EncryptionCapability") =~ /localkey/i
69
+ end
70
+
71
+ # Check if controller encryption is enabled
72
+ def controller_encryption_enabled?(controller)
73
+ return false unless controller
74
+ controller.dig("Oem", "Dell", "DellController", "EncryptionMode") =~ /localkey/i
75
+ end
76
+
77
+ # Get information about physical drives
78
+ def drives(controller)
79
+ raise Error, "Controller not provided" unless controller
80
+
81
+ controller_path = controller["@odata.id"].split("v1/").last
82
+ response = authenticated_request(:get, "/redfish/v1/#{controller_path}?$expand=*($levels=1)")
83
+
84
+ if response.status == 200
85
+ begin
86
+ data = JSON.parse(response.body)
87
+ drives = data["Drives"].map do |body|
88
+ serial = body["SerialNumber"]
89
+ serial = body["Identifiers"].first["DurableName"] if serial.blank?
90
+ {
91
+ serial: serial,
92
+ model: body["Model"],
93
+ name: body["Name"],
94
+ capacity_bytes: body["CapacityBytes"],
95
+ health: body["Status"]["Health"] ? body["Status"]["Health"] : "N/A",
96
+ speed_gbp: body["CapableSpeedGbs"],
97
+ manufacturer: body["Manufacturer"],
98
+ media_type: body["MediaType"],
99
+ failure_predicted: body["FailurePredicted"],
100
+ life_left_percent: body["PredictedMediaLifeLeftPercent"],
101
+ certified: body.dig("Oem", "Dell", "DellPhysicalDisk", "Certified"),
102
+ raid_status: body.dig("Oem", "Dell", "DellPhysicalDisk", "RaidStatus"),
103
+ operation_name: body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationName"),
104
+ operation_progress: body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationPercentCompletePercent"),
105
+ encryption_ability: body["EncryptionAbility"],
106
+ "@odata.id": body["@odata.id"]
107
+ }
108
+ end
109
+
110
+ return drives.sort_by { |d| d[:name] }
111
+ rescue JSON::ParserError
112
+ raise Error, "Failed to parse drives response: #{response.body}"
113
+ end
114
+ else
115
+ raise Error, "Failed to get drives. Status code: #{response.status}"
116
+ end
117
+ end
118
+
119
+ # Get information about virtual disk volumes
120
+ def volumes(controller)
121
+ raise Error, "Controller not provided" unless controller
122
+
123
+ puts "Volumes (e.g. Arrays)".green
124
+
125
+ v = controller["Volumes"]
126
+ path = v["@odata.id"].split("v1/").last
127
+ response = authenticated_request(:get, "/redfish/v1/#{path}?$expand=*($levels=1)")
128
+
129
+ if response.status == 200
130
+ begin
131
+ data = JSON.parse(response.body)
132
+ volumes = data["Members"].map do |vol|
133
+ drives = vol["Links"]["Drives"]
134
+ volume = {
135
+ name: vol["Name"],
136
+ capacity_bytes: vol["CapacityBytes"],
137
+ volume_type: vol["VolumeType"],
138
+ drives: drives,
139
+ write_cache_policy: vol.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy"),
140
+ read_cache_policy: vol.dig("Oem", "Dell", "DellVirtualDisk", "ReadCachePolicy"),
141
+ stripe_size: vol.dig("Oem", "Dell", "DellVirtualDisk", "StripeSize"),
142
+ raid_level: vol["RAIDType"],
143
+ encrypted: vol["Encrypted"],
144
+ lock_status: vol.dig("Oem", "Dell", "DellVirtualDisk", "LockStatus"),
145
+ "@odata.id": vol["@odata.id"]
146
+ }
147
+
148
+ # Check FastPath settings
149
+ volume[:fastpath] = fastpath_good?(volume)
150
+
151
+ # Handle volume operations and status
152
+ if vol["Operations"].any?
153
+ volume[:health] = vol["Status"]["Health"] ? vol["Status"]["Health"] : "N/A"
154
+ volume[:progress] = vol["Operations"].first["PercentageComplete"]
155
+ volume[:message] = vol["Operations"].first["OperationName"]
156
+ elsif vol["Status"]["Health"] == "OK"
157
+ volume[:health] = "OK"
158
+ volume[:progress] = nil
159
+ volume[:message] = nil
160
+ else
161
+ volume[:health] = "?"
162
+ volume[:progress] = nil
163
+ volume[:message] = nil
164
+ end
165
+
166
+ volume
167
+ end
168
+
169
+ return volumes.sort_by { |d| d[:name] }
170
+ rescue JSON::ParserError
171
+ raise Error, "Failed to parse volumes response: #{response.body}"
172
+ end
173
+ else
174
+ raise Error, "Failed to get volumes. Status code: #{response.status}"
175
+ end
176
+ end
177
+
178
+ # Check if FastPath is properly configured for a volume
179
+ def fastpath_good?(volume)
180
+ return "disabled" unless volume
181
+
182
+ # Modern firmware check handled by caller
183
+ if volume[:write_cache_policy] == "WriteThrough" &&
184
+ volume[:read_cache_policy] == "NoReadAhead" &&
185
+ volume[:stripe_size] == "64KB"
186
+ return "enabled"
187
+ else
188
+ return "disabled"
189
+ end
190
+ end
191
+
192
+ # Delete a volume
193
+ def delete_volume(odata_id)
194
+ path = odata_id.split("v1/").last
195
+ puts "Deleting volume: #{path}"
196
+
197
+ response = authenticated_request(:delete, "/redfish/v1/#{path}")
198
+
199
+ if response.status.between?(200, 299)
200
+ puts "Delete volume request sent".green
201
+
202
+ # Check if we need to wait for a job
203
+ if response.headers["location"]
204
+ job_id = response.headers["location"].split("/").last
205
+ wait_for_job(job_id)
206
+ end
207
+
208
+ return true
209
+ else
210
+ error_message = "Failed to delete volume. Status code: #{response.status}"
211
+
212
+ begin
213
+ error_data = JSON.parse(response.body)
214
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
215
+ rescue
216
+ # Ignore JSON parsing errors
217
+ end
218
+
219
+ raise Error, error_message
220
+ end
221
+ end
222
+
223
+ # Create a new virtual disk with RAID5 and FastPath optimizations
224
+ def create_virtual_disk(controller_id, drives, name: "vssd0", raid_type: "RAID5")
225
+ # Check firmware version to determine which API to use
226
+ firmware_version = get_firmware_version.split(".")[0,2].join.to_i
227
+
228
+ # [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)
229
+ payload = {
230
+ "Drives": drives.map { |d| { "@odata.id": d["@odata.id"] } },
231
+ "Name": name,
232
+ "OptimumIOSizeBytes": 64 * 1024,
233
+ "Oem": { "Dell": { "DellVolume": { "DiskCachePolicy": "Enabled" } } },
234
+ "ReadCachePolicy": "Off", # "NoReadAhead"
235
+ "WriteCachePolicy": "WriteThrough"
236
+ }
237
+
238
+ # If the firmware < 440, we need a different approach
239
+ if firmware_version >= 440
240
+ # For modern firmware
241
+ if drives.size < 3 && raid_type == "RAID5"
242
+ puts "*************************************************".red
243
+ puts "* WARNING: Less than 3 drives. Selecting RAID0. *".red
244
+ puts "*************************************************".red
245
+ payload["RAIDType"] = "RAID0"
246
+ else
247
+ payload["RAIDType"] = raid_type
248
+ end
249
+ else
250
+ # For older firmware
251
+ payload["VolumeType"] = "StripedWithParity" if raid_type == "RAID5"
252
+ payload["VolumeType"] = "SpannedDisks" if raid_type == "RAID0"
253
+ end
254
+
255
+ url = "Systems/System.Embedded.1/Storage/#{controller_id}/Volumes"
256
+ response = authenticated_request(
257
+ :post,
258
+ "/redfish/v1/#{url}",
259
+ body: payload.to_json,
260
+ headers: { 'Content-Type': 'application/json' }
261
+ )
262
+
263
+ if response.status.between?(200, 299)
264
+ puts "Virtual disk creation started".green
265
+
266
+ # Check if we need to wait for a job
267
+ if response.headers["location"]
268
+ job_id = response.headers["location"].split("/").last
269
+ wait_for_job(job_id)
270
+ end
271
+
272
+ return true
273
+ else
274
+ error_message = "Failed to create virtual disk. Status code: #{response.status}"
275
+
276
+ begin
277
+ error_data = JSON.parse(response.body)
278
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
279
+ rescue
280
+ # Ignore JSON parsing errors
281
+ end
282
+
283
+ raise Error, error_message
284
+ end
285
+ end
286
+
287
+ # Enable Self-Encrypting Drive support on controller
288
+ def enable_local_key_management(controller_id, passphrase: "Secure123!", keyid: "RAID-Key-2023")
289
+ payload = {
290
+ "TargetFQDD": controller_id,
291
+ "Key": passphrase,
292
+ "Keyid": keyid
293
+ }
294
+
295
+ response = authenticated_request(
296
+ :post,
297
+ "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.SetControllerKey",
298
+ body: payload.to_json,
299
+ headers: { 'Content-Type': 'application/json' }
300
+ )
301
+
302
+ if response.status == 202
303
+ puts "Controller encryption enabled".green
304
+
305
+ # Check if we need to wait for a job
306
+ if response.headers["location"]
307
+ job_id = response.headers["location"].split("/").last
308
+ wait_for_job(job_id)
309
+ end
310
+
311
+ return true
312
+ else
313
+ error_message = "Failed to enable controller encryption. Status code: #{response.status}"
314
+
315
+ begin
316
+ error_data = JSON.parse(response.body)
317
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
318
+ rescue
319
+ # Ignore JSON parsing errors
320
+ end
321
+
322
+ raise Error, error_message
323
+ end
324
+ end
325
+
326
+ # Disable Self-Encrypting Drive support on controller
327
+ def disable_local_key_management(controller_id)
328
+ payload = { "TargetFQDD": controller_id }
329
+
330
+ response = authenticated_request(
331
+ :post,
332
+ "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.RemoveControllerKey",
333
+ body: payload.to_json,
334
+ headers: { 'Content-Type': 'application/json' }
335
+ )
336
+
337
+ if response.status == 202
338
+ puts "Controller encryption disabled".green
339
+
340
+ # Check if we need to wait for a job
341
+ if response.headers["location"]
342
+ job_id = response.headers["location"].split("/").last
343
+ wait_for_job(job_id)
344
+ end
345
+
346
+ return true
347
+ else
348
+ error_message = "Failed to disable controller encryption. Status code: #{response.status}"
349
+
350
+ begin
351
+ error_data = JSON.parse(response.body)
352
+ error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
353
+ rescue
354
+ # Ignore JSON parsing errors
355
+ end
356
+
357
+ raise Error, error_message
358
+ end
359
+ end
360
+
361
+ # Check if all physical disks are Self-Encrypting Drives
362
+ def all_seds?(drives)
363
+ drives.all? { |d| d[:encryption_ability] == "SelfEncryptingDrive" }
364
+ end
365
+
366
+ # Check if the system is ready for SED operations
367
+ def sed_ready?(controller, drives)
368
+ all_seds?(drives) && controller_encryption_capable?(controller) && controller_encryption_enabled?(controller)
369
+ end
370
+
371
+ # Get firmware version
372
+ def get_firmware_version
373
+ response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=FirmwareVersion")
374
+
375
+ if response.status == 200
376
+ begin
377
+ data = JSON.parse(response.body)
378
+ return data["FirmwareVersion"]
379
+ rescue JSON::ParserError
380
+ raise Error, "Failed to parse firmware version response: #{response.body}"
381
+ end
382
+ else
383
+ # Try again without the $select parameter for older firmware
384
+ response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1")
385
+
386
+ if response.status == 200
387
+ begin
388
+ data = JSON.parse(response.body)
389
+ return data["FirmwareVersion"]
390
+ rescue JSON::ParserError
391
+ raise Error, "Failed to parse firmware version response: #{response.body}"
392
+ end
393
+ else
394
+ raise Error, "Failed to get firmware version. Status code: #{response.status}"
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end