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.
- checksums.yaml +4 -4
- data/README.md +13 -0
- data/bin/idrac +374 -93
- data/lib/idrac/boot.rb +186 -79
- data/lib/idrac/client.rb +95 -0
- data/lib/idrac/jobs.rb +8 -4
- data/lib/idrac/license.rb +325 -39
- data/lib/idrac/lifecycle.rb +88 -2
- data/lib/idrac/power.rb +22 -63
- data/lib/idrac/storage.rb +275 -115
- data/lib/idrac/system.rb +227 -130
- data/lib/idrac/system_config.rb +163 -139
- data/lib/idrac/utility.rb +60 -0
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac/virtual_media.rb +19 -29
- data/lib/idrac.rb +2 -0
- metadata +3 -2
data/lib/idrac/system_config.rb
CHANGED
@@ -3,106 +3,170 @@ require 'colorize'
|
|
3
3
|
|
4
4
|
module IDRAC
|
5
5
|
module SystemConfig
|
6
|
-
#
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
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
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
-
#
|
57
|
-
|
121
|
+
# For debugging purposes
|
122
|
+
debug task.inspect, 1, :yellow
|
58
123
|
|
59
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
77
|
-
|
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
|
-
#
|
80
|
-
|
81
|
-
scp = nil
|
148
|
+
# Extract the ID from the location
|
149
|
+
id = location.split("/").last
|
82
150
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
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
data/lib/idrac/virtual_media.rb
CHANGED
@@ -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
|
16
|
-
|
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:
|
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
|
-
|
69
|
-
|
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
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.
|
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
|
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
|