idrac 0.7.1 → 0.7.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.
data/lib/idrac/boot.rb CHANGED
@@ -1,6 +1,31 @@
1
1
  require 'json'
2
2
  require 'colorize'
3
3
 
4
+ ########################################################
5
+ # BIOS Configuration / Boot Order
6
+ ########################################################
7
+ # BEWARE YE WHO ENTER HERE
8
+ # This is the BIOS configuration and boot order section.
9
+ # It is a dark and dangerous place, fraught with peril.
10
+ #
11
+ # BIOS and UEFI and iDRAC all interplay through a handful of REST API calls and
12
+ # a labyrinth of system configuration profile settings. You must know if you are
13
+ # in UEFI or BIOS mode to even know which calls to make and some calls "unlock"
14
+ # only AFTER you make a switch between modes. Which requires an explicit reboot.
15
+ #
16
+ # Two current open issues remain:
17
+ # - How do you avoid booting from an installed USB with a bootable image? (workaround--wipefs the USB)
18
+ # - How do you boot-once to the Virtual CD, install Ubuntu, on its natural reboot step, boot to the HD. (workaround--finish install with poweroff)
19
+ #
20
+ # Get oriented:
21
+ # https://github.com/dell/dellemc-openmanage-ansible-modules/issues/21
22
+ # https://www.dell.com/support/manuals/en-us/openmanage-ansible-modules/user_guide_1_0_1/configuring-bios?guid=guid-d2d8d871-c3e1-48d1-a879-197670fe33ea&lang=en-us
23
+ # https://www.dell.com/support/manuals/en-us/idrac7-8-lifecycle-controller-v2.40.40.40/redfish%202.40.40.40/computersystem?guid=guid-071f0516-1b31-4a4b-90ab-4f9bfcc5db4a&lang=en-us
24
+ # https://infohub.delltechnologies.com/en-US/l/server-configuration-profiles-reference-guide/changing-the-boot-order-2/
25
+ # https://pubs.lenovo.com/xcc-restapi/update_next_onetime_bootconfig_patch
26
+ # https://github.com/dell/iDRAC-Redfish-Scripting/issues/186
27
+ # https://www.dell.com/support/kbdoc/en-us/000198504/boot-device-fqdd-name-changed-in-15g-bios-uefi-boot-sequence-after-bios-update
28
+ # https://github.com/dell/iDRAC-Redfish-Scripting/issues/116
4
29
  module IDRAC
5
30
  module Boot
6
31
  # Get BIOS boot options
@@ -70,31 +95,7 @@ module IDRAC
70
95
  headers: { 'Content-Type': 'application/json' }
71
96
  )
72
97
 
73
- if response.status.between?(200, 299)
74
- puts "UEFI boot mode set. A system reboot is required for changes to take effect.".green
75
-
76
- # Check for job creation
77
- if response.headers["Location"]
78
- job_id = response.headers["Location"].split("/").last
79
- wait_for_job(job_id)
80
- end
81
-
82
- return true
83
- else
84
- error_message = "Failed to set UEFI boot mode. Status code: #{response.status}"
85
-
86
- begin
87
- error_data = JSON.parse(response.body)
88
- if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
89
- error_info = error_data["error"]["@Message.ExtendedInfo"].first
90
- error_message += ", Message: #{error_info['Message']}"
91
- end
92
- rescue
93
- # Ignore JSON parsing errors
94
- end
95
-
96
- raise Error, error_message
97
- end
98
+ wait_for_job(response.headers["location"])
98
99
  end
99
100
  rescue JSON::ParserError
100
101
  raise Error, "Failed to parse BIOS response: #{response.body}"
@@ -103,6 +104,44 @@ module IDRAC
103
104
  raise Error, "Failed to get BIOS information. Status code: #{response.status}"
104
105
  end
105
106
  end
107
+ =begin
108
+ # Servers can boot in BIOS mode or in UEFI (modern, extensible BIOS replacement) mode.
109
+ # We use UEFI mode.
110
+ # self.get(path: "Systems/System.Embedded.1/Bios/Settings?$select=BootMode")
111
+ res = self.get(path: "Systems/System.Embedded.1/Bios")
112
+ if res["body"]["Attributes"]["BootMode"] == "Uefi"
113
+ return { status: :success }
114
+ else
115
+ res = self.set_system_configuration_profile(scp_boot_mode_uefi, reboot: true)
116
+ # Then must power cycle the server
117
+ self.power_on!(wait: true)
118
+ self.power_off!(wait: true)
119
+ return res
120
+ end
121
+
122
+ =end
123
+ def scp_boot_mode_uefi(idrac_license_version: 9)
124
+ opts = { "BootMode" => 'Uefi' }
125
+ # If we're iDRAC 9, we need enable a placeholder, otherwise we can't order the
126
+ # boot order until we've switched to UEFI mode.
127
+ # Read [about it](https://dl.dell.com/manuals/all-products/esuprt_software/esuprt_it_ops_datcentr_mgmt/dell-management-solution-resources_white-papers12_en-us.pdf).
128
+ # ...administrators may wish to reserve a boot entry for a fixed disk in the UEFI Boot Sequence before an OS is installed or before a physical or
129
+ # virtual drive has been formatted. When a HardDisk Drive Placeholder is set to Enabled, the BIOS will create a boot option for the PERC RAID
130
+ # (Integrated or in a PCIe slot) disk if a partition is found, even if there is no FAT filesystem present... this allows the Integrated RAID controller
131
+ # to be moved in the UEFI Boot Sequence prior to the OS installation
132
+ opts["HddPlaceholder"] = "Enabled" if idrac_license_version.to_i == 9
133
+ self.make_scp(fqdd: "BIOS.Setup.1-1", attributes: opts)
134
+ end
135
+ # What triggers a reboot?
136
+ # https://infohub.delltechnologies.com/en-US/l/server-configuration-profiles-reference-guide/host-reboot-2/
137
+ def set_bios(hash)
138
+ scp = self.make_scp(fqdd: "BIOS.Setup.1-1", attributes: hash)
139
+ res = self.set_system_configuration_profile(scp)
140
+ if res[:status] == :success
141
+ self.get_bios_boot_options
142
+ end
143
+ res
144
+ end
106
145
 
107
146
  # Set boot order (HD first)
108
147
  def set_boot_order_hd_first
@@ -165,6 +204,70 @@ module IDRAC
165
204
  raise Error, "Failed to get boot options. Status code: #{boot_options_response.status}"
166
205
  end
167
206
  end
207
+
208
+ def set_uefi_boot_cd_once_then_hd
209
+ boot_options = get_bios_boot_options[:boot_options]
210
+ # Note may have to put device into
211
+ # self.set_bios( { "BootMode" => 'Uefi' } )
212
+ # self.reboot!
213
+ # And then reboot before you can make the following call:
214
+ raid_name = boot_options.include?("RAID.Integrated.1-1") ? "RAID.Integrated.1-1" : "Unknown.Unknown.1-1"
215
+ raise "No RAID HD in boot options" unless boot_options.include?(raid_name)
216
+ bios = {
217
+ "BootMode" => 'Uefi',
218
+ "BootSeqRetry" => "Disabled",
219
+
220
+ # "UefiTargetBootSourceOverride" => 'Cd',
221
+ # "BootSourceOverrideTarget" => 'UefiTarget',
222
+ # "OneTimeBootMode" => "OneTimeUefiBootSeq",
223
+
224
+ # One time boot order
225
+ # "OneTimeHddSeqDev" => "Optical.iDRACVirtual.1-1",
226
+ # "OneTimeBiosBootSeqDev" => "Optical.iDRACVirtual.1-1",
227
+ # "OneTimeUefiBootSeqDev" => "Optical.iDRACVirtual.1-1",
228
+
229
+ # Enabled/Disabled Options
230
+ # "SetBootOrderDis" => "Disk.USBBack.1-1", # Don't boot to USB if it is plugged in
231
+ "SetBootOrderEn" => raid_name,
232
+ # "SetBootOrderFqdd1" => raid_name,
233
+ # "SetLegacyHddOrderFqdd1" => raid_name,
234
+ # "SetBootOrderFqdd2" => "Optical.iDRACVirtual.1-1",
235
+
236
+ # Permanent Boot Order
237
+ "HddSeq" => raid_name,
238
+ "BiosBootSeq" => raid_name,
239
+ "UefiBootSeq" => raid_name # This is likely redundant...
240
+ }
241
+ # The usb device will have 'usb' in it:
242
+ usb_name = boot_options.select { |b| b =~ /usb/i }
243
+ bios["SetBootOrderDis"] = usb_name if usb_name.present?
244
+
245
+ set_bios(bios)
246
+ end
247
+
248
+ # This sets boot to HD but before that it sets the one-time boot to CD
249
+ # Different approach for iDRAC 8 vs 9
250
+ def override_boot_source
251
+ # For now try with all iDRAC versions
252
+ if self.license_version.to_i == 9
253
+ set_boot_order_hd_first()
254
+ set_one_time_virtual_media_boot()
255
+ else
256
+ scp = {"FQDD"=>"iDRAC.Embedded.1", "Attributes"=> [{"Name"=>"ServerBoot.1#BootOnce", "Value"=>"Enabled", "Set On Import"=>"True"}, {"Name"=>"ServerBoot.1#FirstBootDevice", "Value"=>"VCD-DVD", "Set On Import"=>"True"}]}
257
+ # set_uefi_boot_cd_once_then_hd
258
+ # scp = self.set_bios_boot_cd_first
259
+ # get_bios_boot_options # Make sure we know if the OS is calling it Unknown or RAID
260
+ # {"FQDD"=>"BIOS.Setup.1-1", "Attributes"=>
261
+ # [{"Name"=>"ServerBoot.1#BootOnce", "Value"=>"Enabled", "Set On Import"=>"True"},
262
+ # {"Name"=>"ServerBoot.1#FirstBootDevice", "Value"=>"VCD-DVD", "Set On Import"=>"True"},
263
+ # {"Name"=>"BootSeqRetry", "Value"=>"Disabled", "Set On Import"=>"True"},
264
+ # {"Name"=>"UefiBootSeq", "Value"=>"Unknown.Unknown.1-1,NIC.PxeDevice.1-1,Floppy.iDRACVirtual.1-1,Optical.iDRACVirtual.1-1",
265
+ # "Set On Import"=>"True"}]}
266
+
267
+ # 3.3.0 :018 > scp1 = {"FQDD"=>"BIOS.Setup.1-1", "Attributes"=> [{"Name"=>"OneTimeUefiBootSeq", "Value"=>"VCD-DVD", "Set On Import"=>"True"}, {"Name"=>"BootSeqRetry", "Value"=>"Disabled", "Set On Import"=>"True"}, {"Name"=>"UefiBootSeq", "Value"=>"Unknown.Unknown.1-1,NIC.PxeDevice.1-1", "Set On Import"=>"True"}]}
268
+ set_system_configuration_profile(scp) # This will cycle power and leave the device off.
269
+ end
270
+ end
168
271
 
169
272
  # Configure BIOS settings
170
273
  def configure_bios_settings(settings)
@@ -221,6 +324,60 @@ module IDRAC
221
324
  })
222
325
  end
223
326
 
327
+ # Check if BIOS error prompt is disabled
328
+ def bios_error_prompt_disabled?
329
+ response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
330
+
331
+ if response.status == 200
332
+ begin
333
+ data = JSON.parse(response.body)
334
+ if data["Attributes"] && data["Attributes"].has_key?("ErrPrompt")
335
+ return data["Attributes"]["ErrPrompt"] == "Disabled"
336
+ else
337
+ debug "ErrPrompt attribute not found in BIOS settings", 1, :yellow
338
+ return false
339
+ end
340
+ rescue JSON::ParserError
341
+ debug "Failed to parse BIOS response", 0, :red
342
+ return false
343
+ end
344
+ else
345
+ debug "Failed to get BIOS information. Status code: #{response.status}", 0, :red
346
+ return false
347
+ end
348
+ end
349
+
350
+ def bios_hdd_placeholder_enabled?
351
+ case self.license_version.to_i
352
+ when 8
353
+ # scp = usable_scp(get_system_configuration_profile(target: "BIOS"))
354
+ # scp["BIOS.Setup.1-1"]["HddPlaceholder"] == "Enabled"
355
+ true
356
+ else
357
+ response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
358
+ json = JSON.parse(response.body)
359
+ raise "Error reading HddPlaceholder setup" if json&.dig('SystemConfiguration').blank?
360
+ json["Attributes"]["HddPlaceholder"] == "Enabled"
361
+ end
362
+ end
363
+
364
+ def bios_os_power_control_enabled?
365
+ case self.license_version.to_i
366
+ when 8
367
+ scp = usable_scp(get_system_configuration_profile(target: "BIOS"))
368
+ scp["BIOS.Setup.1-1"]["ProcCStates"] == "Enabled" &&
369
+ scp["BIOS.Setup.1-1"]["SysProfile"] == "PerfPerWattOptimizedOs" &&
370
+ scp["BIOS.Setup.1-1"]["ProcPwrPerf"] == "OsDbpm"
371
+ else
372
+ response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
373
+ json = JSON.parse(response.body)
374
+ raise "Error reading PowerControl setup" if json&.dig('SystemConfiguration').blank?
375
+ json["Attributes"]["ProcCStates"] == "Enabled" &&
376
+ json["Attributes"]["SysProfile"] == "PerfPerWattOptimizedOs" &&
377
+ json["Attributes"]["ProcPwrPerf"] == "OsDbpm"
378
+ end
379
+ end
380
+
224
381
  # Get iDRAC version - needed for boot management differences
225
382
  def get_idrac_version
226
383
  response = authenticated_request(:get, "/redfish/v1")
@@ -288,7 +445,6 @@ module IDRAC
288
445
  "Target": target
289
446
  }
290
447
  }
291
-
292
448
  # Configure shutdown behavior
293
449
  params["ShutdownType"] = "Forced"
294
450
  params["HostPowerState"] = reboot ? "On" : "Off"
@@ -300,58 +456,9 @@ module IDRAC
300
456
  headers: { 'Content-Type': 'application/json' }
301
457
  )
302
458
 
303
- if response.status.between?(200, 299)
304
- # Check if we need to wait for a job
305
- if response.headers["location"]
306
- job_id = response.headers["location"].split("/").last
307
-
308
- job = wait_for_job(job_id)
309
-
310
- # Check for task completion status
311
- if job["TaskState"] == "Completed" && job["TaskStatus"] == "OK"
312
- puts "System configuration imported successfully".green
313
- return true
314
- else
315
- # If there's an error message with a line number, surface it
316
- error_message = "Failed to import system configuration"
317
-
318
- if job["Messages"]
319
- job["Messages"].each do |m|
320
- puts "#{m["Message"]} (#{m["Severity"]})".red
321
-
322
- # Check for line number in error message
323
- if m["Message"] =~ /line (\d+)/
324
- line_num = $1.to_i
325
- lines = JSON.pretty_generate(scp).split("\n")
326
- puts "Error near line #{line_num}:".red
327
- ((line_num-3)..(line_num+1)).each do |ln|
328
- puts "#{ln}: #{lines[ln-1]}" if ln > 0 && ln <= lines.length
329
- end
330
- end
331
- end
332
- end
333
-
334
- raise Error, error_message
335
- end
336
- else
337
- puts "System configuration import started, but no job ID was returned".yellow
338
- return true
339
- end
340
- else
341
- error_message = "Failed to import system configuration. Status code: #{response.status}"
342
-
343
- begin
344
- error_data = JSON.parse(response.body)
345
- if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
346
- error_info = error_data["error"]["@Message.ExtendedInfo"].first
347
- error_message += ", Message: #{error_info['Message']}"
348
- end
349
- rescue
350
- # Ignore JSON parsing errors
351
- end
352
-
353
- raise Error, error_message
354
- end
459
+ task = wait_for_task(response.headers["location"])
460
+ debugger
461
+ return task
355
462
  end
356
463
  end
357
- end
464
+ end
data/lib/idrac/client.rb CHANGED
@@ -23,6 +23,7 @@ module IDRAC
23
23
  include Boot
24
24
  include License
25
25
  include SystemConfig
26
+ include Utility
26
27
 
27
28
  def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, auto_delete_sessions: true, retry_count: 3, retry_delay: 1)
28
29
  @host = host
@@ -357,6 +358,100 @@ module IDRAC
357
358
  raise e
358
359
  end
359
360
  end
361
+ end # Wait for a task to complete
362
+
363
+ def wait_for_task(task_id)
364
+ task = nil
365
+
366
+ begin
367
+ loop do
368
+ task_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{task_id}")
369
+
370
+ case task_response.status
371
+ # 200-299
372
+ when 200..299
373
+ task = JSON.parse(task_response.body)
374
+
375
+ if task["TaskState"] != "Running"
376
+ break
377
+ end
378
+
379
+ # Extract percentage complete if available
380
+ percent_complete = nil
381
+ if task["Oem"] && task["Oem"]["Dell"] && task["Oem"]["Dell"]["PercentComplete"]
382
+ percent_complete = task["Oem"]["Dell"]["PercentComplete"]
383
+ debug "Task progress: #{percent_complete}% complete", 1
384
+ end
385
+
386
+ debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
387
+ sleep 5
388
+ else
389
+ return {
390
+ status: :failed,
391
+ error: "Failed to check task status: #{task_response.status} - #{task_response.body}"
392
+ }
393
+ end
394
+ end
395
+
396
+ # Check final task state
397
+ if task["TaskState"] == "Completed" && task["TaskStatus"] == "OK"
398
+ debugger
399
+ return { status: :success }
400
+ elsif task["SystemConfiguration"] # SystemConfigurationProfile requests yield a 202 with a SystemConfiguration key
401
+ return task
402
+ else
403
+ # For debugging purposes
404
+ debug task.inspect, 1, :yellow
405
+
406
+ # Extract any messages from the response
407
+ messages = []
408
+ if task["Messages"] && task["Messages"].is_a?(Array)
409
+ messages = task["Messages"].map { |m| m["Message"] }.compact
410
+ end
411
+
412
+ return {
413
+ status: :failed,
414
+ task_state: task["TaskState"],
415
+ task_status: task["TaskStatus"],
416
+ messages: messages,
417
+ error: messages.first || "Task failed with state: #{task["TaskState"]}"
418
+ }
419
+ end
420
+ rescue => e
421
+ debugger
422
+ return { status: :error, error: "Exception monitoring task: #{e.message}" }
423
+ end
424
+ end
425
+
426
+ def handle_response(response)
427
+ # First see if there is a location header
428
+ if response.headers["location"]
429
+ return handle_location(response.headers["location"])
430
+ end
431
+
432
+ # If there is no location header, check the status code
433
+ if response.status.between?(200, 299)
434
+ return response.body
435
+ else
436
+ raise Error, "Failed to #{response.status} - #{response.body}"
437
+ end
438
+ end
439
+
440
+ # Handle location header and determine whether to use wait_for_job or wait_for_task
441
+ def handle_location(location)
442
+ return nil if location.nil? || location.empty?
443
+
444
+ # Extract the ID from the location
445
+ id = location.split("/").last
446
+
447
+ # Determine if it's a task or job based on the URL pattern
448
+ if location.include?("/TaskService/Tasks/")
449
+ wait_for_task(id)
450
+ else
451
+ # Assuming it's a job
452
+ wait_for_job(id)
453
+ end
360
454
  end
455
+
361
456
  end
362
457
  end
data/lib/idrac/jobs.rb CHANGED
@@ -28,8 +28,8 @@ module IDRAC
28
28
  if response.status == 200
29
29
  begin
30
30
  jobs_data = JSON.parse(response.body)
31
- jobs_data["Members"].each do |job|
32
- puts "#{job['Id']} : #{job['JobState']} > #{job['Message']}"
31
+ jobs_data["Members"].each.with_index do |job, i |
32
+ puts "#{job['Id']} : #{job['JobState']} > #{job['Message']} <#{job['CompletionTime']}> [#{i+1}/#{jobs_data["Members"].count}]"
33
33
  end
34
34
  return jobs_data
35
35
  rescue JSON::ParserError
@@ -44,6 +44,7 @@ module IDRAC
44
44
  def clear_jobs!
45
45
  # Get list of jobs
46
46
  jobs_response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)')
47
+ handle_response(jobs_response)
47
48
 
48
49
  if jobs_response.status == 200
49
50
  begin
@@ -52,7 +53,7 @@ module IDRAC
52
53
 
53
54
  # Delete each job individually
54
55
  members.each.with_index do |job, i|
55
- puts "Removing #{job['Id']} [#{i+1}/#{members.count}]"
56
+ puts "Removing #{job['Id']} : #{job['JobState']} > #{job['Message']} [#{i+1}/#{members.count}]".yellow
56
57
  delete_response = authenticated_request(:delete, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{job['Id']}")
57
58
 
58
59
  unless delete_response.status.between?(200, 299)
@@ -89,7 +90,7 @@ module IDRAC
89
90
  # Monitor LC status until it's Ready
90
91
  puts "Waiting for LC status to be Ready..."
91
92
 
92
- retries = 12 # ~2 minutes with 10s sleep
93
+ retries = 60 # ~10 minutes with 10s sleep
93
94
  while retries > 0
94
95
  lc_response = authenticated_request(
95
96
  :post,
@@ -163,12 +164,15 @@ module IDRAC
163
164
  case job_state
164
165
  when "Completed"
165
166
  puts "Job completed successfully".green
167
+ puts "CompletionTime: #{job_data['CompletionTime']}".green if job_data['CompletionTime']
166
168
  return job_data
167
169
  when "Failed"
168
170
  puts "Job failed: #{job_data['Message']}".red
171
+ puts "CompletionTime: #{job_data['CompletionTime']}".red if job_data['CompletionTime']
169
172
  raise Error, "Job failed: #{job_data['Message']}"
170
173
  when "CompletedWithErrors"
171
174
  puts "Job completed with errors: #{job_data['Message']}".yellow
175
+ puts "CompletionTime: #{job_data['CompletionTime']}".yellow if job_data['CompletionTime']
172
176
  return job_data
173
177
  end
174
178