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.
@@ -3,106 +3,170 @@ require 'colorize'
3
3
 
4
4
  module IDRAC
5
5
  module SystemConfig
6
- # Get the system configuration profile for a given target (e.g. "RAID")
7
- def get_system_configuration_profile(target: "RAID")
8
- tries = 0
9
- location = nil
10
- started_at = Time.now
6
+ # This assigns the iDRAC IP to be a STATIC IP.
7
+ def set_idrac_ip(new_ip:, new_gw:, new_nm:, vnc_password: "calvin")
8
+ scp = get_system_configuration_profile(target: "iDRAC")
9
+ pp scp
10
+ ## We want to access the iDRAC web server even when IPs don't match (and they won't when we port forward local host):
11
+ set_scp_attribute(scp, "WebServer.1#HostHeaderCheck", "Disabled")
12
+ ## We want VirtualMedia to be enabled so we can mount ISOs: set_scp_attribute(scp, "VirtualMedia.1#Enable", "Enabled")
13
+ set_scp_attribute(scp, "VirtualMedia.1#EncryptEnable", "Disabled")
14
+ ## We want to access VNC Server on 5901 for screenshots and without SSL:
15
+ set_scp_attribute(scp, "VNCServer.1#Enable", "Enabled")
16
+ set_scp_attribute(scp, "VNCServer.1#Port", "5901")
17
+ set_scp_attribute(scp, "VNCServer.1#SSLEncryptionBitLength", "Disabled")
18
+ # And password calvin
19
+ set_scp_attribute(scp, "VNCServer.1#Password", vnc_password)
20
+ # Disable DHCP on management NIC
21
+ set_scp_attribute(scp, "IPv4.1#DHCPEnable", "Disabled")
22
+ if drac_license_version.to_i == 8
23
+ # We want to use HTML for the virtual console
24
+ set_scp_attribute(scp, "VirtualConsole.1#PluginType", "HTML5")
25
+ # We want static IP for the iDRAC
26
+ set_scp_attribute(scp, "IPv4.1#Address", new_ip)
27
+ set_scp_attribute(scp, "IPv4.1#Gateway", new_gw)
28
+ set_scp_attribute(scp, "IPv4.1#Netmask", new_nm)
29
+ elsif drac_license_version.to_i == 9
30
+ # We want static IP for the iDRAC
31
+ set_scp_attribute(scp, "IPv4Static.1#Address", new_ip)
32
+ set_scp_attribute(scp, "IPv4Static.1#Gateway", new_gw)
33
+ set_scp_attribute(scp, "IPv4Static.1#Netmask", new_nm)
34
+ # {"Name"=>"SerialCapture.1#Enable", "Value"=>"Disabled", "Set On Import"=>"True", "Comment"=>"Read and Write"},
35
+ else
36
+ raise "Unknown iDRAC version"
37
+ end
38
+ while true
39
+ res = self.post(path: "Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration", params: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}})
40
+ # A successful JOB will have a location header with a job id.
41
+ # We can get a busy message instead if we've sent too many iDRAC jobs back-to-back, so we check for that here.
42
+ if res[:headers]["location"].present?
43
+ # We have a job id, so we're good to go.
44
+ break
45
+ else
46
+ # Depending on iDRAC version content-length may be present or not.
47
+ # res[:headers]["content-length"].blank?
48
+ msg = res['body']['error']['@Message.ExtendedInfo'].first['Message']
49
+ details = res['body']['error']['@Message.ExtendedInfo'].first['Resolution']
50
+ # msg => "A job operation is already running. Retry the operation after the existing job is completed."
51
+ # details => "Wait until the running job is completed or delete the scheduled job and retry the operation."
52
+ if details =~ /Wait until the running job is completed/
53
+ sleep 10
54
+ else
55
+ Rails.logger.warn msg+details
56
+ raise "failed configuring static ip, message: #{msg}, details: #{details}"
57
+ end
58
+ end
59
+ end
11
60
 
12
- while location.nil?
13
- debug "Exporting System Configuration try #{tries+=1}..."
14
-
15
- response = authenticated_request(:post,
16
- "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration",
17
- body: {"ExportFormat": "JSON", "ShareParameters":{"Target": target}}.to_json,
18
- headers: {"Content-Type" => "application/json"}
19
- )
61
+ # Allow some time for the iDRAC to prepare before checking the task status
62
+ sleep 3
63
+
64
+ # Use handle_location to monitor task progress
65
+ result = handle_location(res[:headers]["location"])
66
+
67
+ # Check if the operation succeeded
68
+ if result[:status] != :success
69
+ # Extract error details if available
70
+ message = result[:messages].first rescue "N/A"
71
+ error = result[:error] || "Unknown error"
72
+ raise "Failed configuring static IP: #{message} - #{error}"
73
+ end
74
+
75
+ # Finally, let's update our configuration to reflect the new port:
76
+ self.idrac
77
+ return true
78
+ end
20
79
 
21
- if response.status == 400
22
- debug "Failed exporting system configuration: #{response.body}", 1, :red
23
- raise Error, "Failed exporting system configuration profile"
24
- elsif response.status.between?(401, 599)
25
- debug "Failed exporting system configuration: #{response.body}", 1, :red
26
-
27
- # Parse error response
28
- error_data = JSON.parse(response.body) rescue nil
80
+ # Wait for a task to complete
81
+ def wait_for_task(task_id)
82
+ task = nil
83
+
84
+ begin
85
+ loop do
86
+ task_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{task_id}")
29
87
 
30
- if error_data && error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
31
- message_info = error_data["error"]["@Message.ExtendedInfo"]
88
+ case task_response.status
89
+ # 200-299
90
+ when 200..299
91
+ task = JSON.parse(task_response.body)
92
+
93
+ if task["TaskState"] != "Running"
94
+ break
95
+ end
32
96
 
33
- # Check for specific error conditions
34
- if message_info.any? { |m| m["Message"] =~ /existing configuration job is already in progress/ }
35
- debug "Existing configuration job is already in progress, retrying...", 1, :yellow
36
- sleep 30
37
- elsif message_info.any? { |m| m["Message"] =~ /job operation is already running/ }
38
- debug "Existing job operation is already in progress, retrying...", 1, :yellow
39
- sleep 60
40
- else
41
- # Detailed error info for debugging
42
- debug "*" * 80, 1, :red
43
- debug "Headers: #{response.headers.inspect}", 1, :red
44
- debug "Body: #{response.body}", 1, :yellow
45
-
46
- # Extract the first error message if available
47
- error_message = message_info.first["Message"] rescue "Unknown error"
48
- debug "Error: #{error_message}", 1, :red
49
-
50
- raise Error, "Failed to export SCP: #{error_message}"
97
+ # Extract percentage complete if available
98
+ percent_complete = nil
99
+ if task["Oem"] && task["Oem"]["Dell"] && task["Oem"]["Dell"]["PercentComplete"]
100
+ percent_complete = task["Oem"]["Dell"]["PercentComplete"]
101
+ debug "Task progress: #{percent_complete}% complete", 1
51
102
  end
103
+
104
+ debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
105
+ sleep 5
52
106
  else
53
- raise Error, "Failed to export SCP with status #{response.status}"
107
+ return {
108
+ status: :failed,
109
+ error: "Failed to check task status: #{task_response.status} - #{task_response.body}"
110
+ }
54
111
  end
112
+ end
113
+
114
+ # Check final task state
115
+ if task["TaskState"] == "Completed" && task["TaskStatus"] == "OK"
116
+ debugger
117
+ return { status: :success }
118
+ elsif task["SystemConfiguration"] # SystemConfigurationProfile requests yield a 202 with a SystemConfiguration key
119
+ return task
55
120
  else
56
- # Success path - extract location header
57
- location = response.headers["location"]
121
+ # For debugging purposes
122
+ debug task.inspect, 1, :yellow
58
123
 
59
- if location.nil? || location.empty?
60
- raise Error, "Empty location header in response: #{response.headers.inspect}"
124
+ # Extract any messages from the response
125
+ messages = []
126
+ if task["Messages"] && task["Messages"].is_a?(Array)
127
+ messages = task["Messages"].map { |m| m["Message"] }.compact
61
128
  end
129
+
130
+ return {
131
+ status: :failed,
132
+ task_state: task["TaskState"],
133
+ task_status: task["TaskStatus"],
134
+ messages: messages,
135
+ error: messages.first || "Task failed with state: #{task["TaskState"]}"
136
+ }
62
137
  end
63
-
64
- # Progress reporting
65
- minutes_elapsed = ((Time.now - started_at).to_f / 60).to_i
66
- debug "Waiting for export to complete... #{minutes_elapsed} minutes", 1, :yellow
67
-
68
- # Exponential backoff
69
- sleep 2**[tries, 6].min # Cap at 64 seconds
70
-
71
- if tries > 10
72
- raise Error, "Failed exporting SCP after #{tries} tries, location: #{location}"
73
- end
138
+ rescue => e
139
+ debugger
140
+ return { status: :error, error: "Exception monitoring task: #{e.message}" }
74
141
  end
142
+ end
75
143
 
76
- # Extract job ID from location
77
- job_id = location.split("/").last
144
+ # Handle location header and determine whether to use wait_for_job or wait_for_task
145
+ def handle_location(location)
146
+ return nil if location.nil? || location.empty?
78
147
 
79
- # Poll for job completion
80
- job_complete = false
81
- scp = nil
148
+ # Extract the ID from the location
149
+ id = location.split("/").last
82
150
 
83
- while !job_complete
84
- job_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}")
85
-
86
- if job_response.status == 200
87
- job_data = JSON.parse(job_response.body)
88
-
89
- if ["Running", "Pending", "New"].include?(job_data["TaskState"])
90
- debug "Job status: #{job_data["TaskState"]}, waiting...", 2
91
- sleep 3
92
- else
93
- job_complete = true
94
- scp = job_data
95
-
96
- # Verify we have the system configuration data
97
- unless scp["SystemConfiguration"]
98
- raise Error, "Failed exporting SCP, taskstate: #{scp["TaskState"]}, taskstatus: #{scp["TaskStatus"]}"
99
- end
100
- end
101
- else
102
- raise Error, "Failed to check job status: #{job_response.status} - #{job_response.body}"
103
- end
151
+ # Determine if it's a task or job based on the URL pattern
152
+ if location.include?("/TaskService/Tasks/")
153
+ wait_for_task(id)
154
+ else
155
+ # Assuming it's a job
156
+ wait_for_job(id)
104
157
  end
105
-
158
+ end
159
+
160
+ # Get the system configuration profile for a given target (e.g. "RAID")
161
+ def get_system_configuration_profile(target: "RAID")
162
+ debug "Exporting System Configuration..."
163
+ response = authenticated_request(:post,
164
+ "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration",
165
+ body: {"ExportFormat": "JSON", "ShareParameters":{"Target": target}}.to_json,
166
+ headers: {"Content-Type" => "application/json"}
167
+ )
168
+ scp = handle_location(response.headers["location"])
169
+ raise(Error, "Failed exporting SCP, taskstate: #{scp["TaskState"]}, taskstatus: #{scp["TaskStatus"]}") unless scp["SystemConfiguration"]
106
170
  return scp
107
171
  end
108
172
 
@@ -207,61 +271,21 @@ module IDRAC
207
271
  return { status: :failed, error: "Failed importing SCP: #{response.body}" }
208
272
  end
209
273
 
210
- # Get the job location
211
- job_location = response.headers["location"]
212
- if job_location.nil? || job_location.empty?
213
- debug response.inspect, 1, :blue
214
- return { status: :failed, error: "Failed importing SCP... invalid iDRAC response" }
215
- end
216
-
217
- # Extract job ID and monitor the task
218
- job_id = job_location.split("/").last
219
- task = nil
220
-
221
- begin
222
- loop do
223
- task_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}")
224
-
225
- if task_response.status == 200
226
- task = JSON.parse(task_response.body)
227
-
228
- if task["TaskState"] != "Running"
229
- break
230
- end
231
-
232
- debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
233
- sleep 5
234
- else
235
- return {
236
- status: :failed,
237
- error: "Failed to check task status: #{task_response.status} - #{task_response.body}"
238
- }
239
- end
240
- end
241
-
242
- # Check final task state
243
- if task["TaskState"] == "Completed" && task["TaskStatus"] == "OK"
244
- return { status: :success }
245
- else
246
- # For debugging purposes
247
- debug task.inspect, 1, :yellow
248
-
249
- # Extract any messages from the response
250
- messages = []
251
- if task["Messages"] && task["Messages"].is_a?(Array)
252
- messages = task["Messages"].map { |m| m["Message"] }.compact
253
- end
254
-
255
- return {
256
- status: :failed,
257
- task_state: task["TaskState"],
258
- task_status: task["TaskStatus"],
259
- messages: messages,
260
- error: messages.first || "Task failed with state: #{task["TaskState"]}"
261
- }
274
+ return handle_location(response.headers["location"])
275
+ end
276
+
277
+ # This puts the SCP into a format that can be used by reasonable Ruby code.
278
+ # It's a hash of FQDDs to attributes.
279
+ def usable_scp(scp)
280
+ # { "FQDD1" => { "Name" => "Value" }, "FQDD2" => { "Name" => "Value" } }
281
+ scp.dig("SystemConfiguration", "Components").inject({}) do |acc, component|
282
+ fqdd = component["FQDD"]
283
+ attributes = component["Attributes"]
284
+ acc[fqdd] = attributes.inject({}) do |attr_acc, attr|
285
+ attr_acc[attr["Name"]] = attr["Value"]
286
+ attr_acc
262
287
  end
263
- rescue => e
264
- return { status: :error, error: "Exception monitoring task: #{e.message}" }
288
+ acc
265
289
  end
266
290
  end
267
291
 
@@ -366,4 +390,4 @@ module IDRAC
366
390
  result
367
391
  end
368
392
  end
369
- end
393
+ end
@@ -0,0 +1,60 @@
1
+ module IDRAC
2
+ module Utility
3
+ include Debuggable
4
+
5
+ # Reset the iDRAC controller (graceful restart)
6
+ def reset!
7
+ debug "Resetting iDRAC controller...", 1
8
+
9
+ response = authenticated_request(
10
+ :post,
11
+ "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset",
12
+ body: { "ResetType" => "GracefulRestart" }.to_json,
13
+ headers: { 'Content-Type' => 'application/json' }
14
+ )
15
+
16
+ if response.status.between?(200, 299)
17
+ debug "Reset command accepted, waiting for iDRAC to restart...", 1, :green
18
+ tries = 0
19
+
20
+ while true
21
+ begin
22
+ debug "Checking if iDRAC is back online...", 1
23
+ response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1")
24
+ if response.status.between?(200, 299)
25
+ debug "iDRAC is back online!", 1, :green
26
+ break
27
+ end
28
+ sleep 30
29
+ rescue => e
30
+ tries += 1
31
+ if tries > 5
32
+ debug "Failed to reconnect to iDRAC after 5 attempts", 1, :red
33
+ return false
34
+ end
35
+ debug "No response from server... retry #{tries}/5", 1, :red
36
+ sleep 2 ** tries
37
+ end
38
+ end
39
+ else
40
+ begin
41
+ error_data = JSON.parse(response.body)
42
+ if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
43
+ message = error_data["error"]["@Message.ExtendedInfo"].first["Message"]
44
+ debug "*" * 80, 1, :red
45
+ debug message, 1, :red
46
+ debug "*" * 80, 1, :red
47
+ else
48
+ debug "Failed to reset iDRAC. Status code: #{response.status}", 1, :red
49
+ end
50
+ rescue => e
51
+ debug "Failed to reset iDRAC. Status code: #{response.status}", 1, :red
52
+ debug "Error response: #{response.body}", 2, :red
53
+ end
54
+ return false
55
+ end
56
+
57
+ true
58
+ end
59
+ end
60
+ end
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.7.1"
4
+ VERSION = "0.7.2"
5
5
  end
@@ -12,8 +12,20 @@ module IDRAC
12
12
  data = JSON.parse(response.body)
13
13
 
14
14
  media = data["Members"].map do |m|
15
- if m["Inserted"]
16
- puts "#{m["Name"]} #{m["ConnectedVia"]} #{m["Image"]}".green
15
+ # Check if media is inserted based on multiple indicators
16
+ is_inserted = m["Inserted"] ||
17
+ (!m["Image"].nil? && !m["Image"].empty?) ||
18
+ (m["ConnectedVia"] && m["ConnectedVia"] != "NotConnected") ||
19
+ (m["ImageName"] && !m["ImageName"].empty?)
20
+
21
+ # Indicate which field is used for this iDRAC version and print it
22
+ puts "ImageName is used for this iDRAC version".yellow if m["ImageName"]
23
+ puts "Image is used for this iDRAC version".yellow if m["Image"]
24
+ puts "ConnectedVia is used for this iDRAC version".yellow if m["ConnectedVia"]
25
+ puts "Inserted is used for this iDRAC version".yellow if m["Inserted"]
26
+
27
+ if is_inserted
28
+ puts "#{m["Name"]} #{m["ConnectedVia"]} #{m["Image"] || m["ImageName"]}".green
17
29
  else
18
30
  puts "#{m["Name"]} #{m["ConnectedVia"]}".yellow
19
31
  end
@@ -22,8 +34,8 @@ module IDRAC
22
34
 
23
35
  {
24
36
  device: m["Id"],
25
- inserted: m["Inserted"],
26
- image: m["Image"] || m["ConnectedVia"],
37
+ inserted: is_inserted,
38
+ image: m["Image"] || m["ImageName"] || m["ConnectedVia"],
27
39
  action_path: action_path
28
40
  }
29
41
  end
@@ -64,31 +76,9 @@ module IDRAC
64
76
  body: {}.to_json,
65
77
  headers: { 'Content-Type': 'application/json' }
66
78
  )
67
-
68
- case response.status
69
- when 200..299
70
- sleep 5 # Wait for ejection to complete
71
- puts "Ejected #{media_to_eject[:device]}".green
72
- return true
73
- when 500..599
74
- # Check if the error is "No Virtual Media devices are currently connected"
75
- begin
76
- error_data = JSON.parse(response.body)
77
- if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"] &&
78
- error_data["error"]["@Message.ExtendedInfo"].any? { |m| m["Message"] =~ /No Virtual Media devices are currently connected/ }
79
- puts "No Virtual Media devices are currently connected".yellow
80
- return false
81
- end
82
- rescue JSON::ParserError
83
- # Ignore parsing errors
84
- end
85
-
86
- puts "Failed to eject media: #{response.status}".red
87
- return false
88
- else
89
- puts "Unexpected response code: #{response.status}".red
90
- return false
91
- end
79
+
80
+ handle_response(response)
81
+ response.status.between?(200, 299)
92
82
  end
93
83
 
94
84
  # Insert virtual media (ISO)
data/lib/idrac.rb CHANGED
@@ -53,4 +53,6 @@ require 'idrac/virtual_media'
53
53
  require 'idrac/boot'
54
54
  require 'idrac/license'
55
55
  require 'idrac/system_config'
56
+ require 'idrac/utility'
57
+ # Client include must come last:
56
58
  require 'idrac/client'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-25 00:00:00.000000000 Z
11
+ date: 2025-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -277,6 +277,7 @@ files:
277
277
  - lib/idrac/storage.rb
278
278
  - lib/idrac/system.rb
279
279
  - lib/idrac/system_config.rb
280
+ - lib/idrac/utility.rb
280
281
  - lib/idrac/version.rb
281
282
  - lib/idrac/virtual_media.rb
282
283
  - lib/idrac/web.rb