idrac 0.1.40 → 0.1.60
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 +75 -8
- data/bin/idrac +244 -86
- data/lib/idrac/client.rb +54 -3
- data/lib/idrac/firmware.rb +26 -2
- data/lib/idrac/jobs.rb +212 -0
- data/lib/idrac/lifecycle.rb +300 -0
- data/lib/idrac/power.rb +195 -0
- data/lib/idrac/session.rb +505 -66
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac.rb +29 -8
- metadata +5 -2
@@ -0,0 +1,300 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module LifecycleMethods
|
6
|
+
# Get the Lifecycle Controller status
|
7
|
+
def get_lifecycle_status
|
8
|
+
# Try first method (older iDRACs)
|
9
|
+
path1 = '/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.GetRemoteServicesAPIStatus'
|
10
|
+
|
11
|
+
begin
|
12
|
+
response = authenticated_request(
|
13
|
+
:post,
|
14
|
+
path1,
|
15
|
+
body: {}.to_json,
|
16
|
+
headers: { 'Content-Type' => 'application/json' }
|
17
|
+
)
|
18
|
+
|
19
|
+
if response.status.between?(200, 299)
|
20
|
+
begin
|
21
|
+
lc_data = JSON.parse(response.body)
|
22
|
+
puts "LC Status: #{lc_data['LCStatus']}".light_cyan
|
23
|
+
return lc_data
|
24
|
+
rescue JSON::ParserError
|
25
|
+
# Fall through to alternative method
|
26
|
+
end
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
# Fall through to alternative method
|
30
|
+
end
|
31
|
+
|
32
|
+
# Try alternative method (newer iDRACs)
|
33
|
+
path2 = '/redfish/v1/Managers/iDRAC.Embedded.1/Attributes'
|
34
|
+
|
35
|
+
begin
|
36
|
+
response = authenticated_request(:get, path2)
|
37
|
+
|
38
|
+
if response.status.between?(200, 299)
|
39
|
+
begin
|
40
|
+
attributes_data = JSON.parse(response.body)
|
41
|
+
|
42
|
+
if attributes_data["Attributes"] && attributes_data["Attributes"]["LCAttributes.1.LifecycleControllerState"]
|
43
|
+
lifecycle_state = attributes_data["Attributes"]["LCAttributes.1.LifecycleControllerState"]
|
44
|
+
puts "Lifecycle Controller state: #{lifecycle_state}".light_cyan
|
45
|
+
return { "LCStatus" => lifecycle_state }
|
46
|
+
end
|
47
|
+
rescue JSON::ParserError
|
48
|
+
# Fall through to final error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
rescue => e
|
52
|
+
# Fall through to final error
|
53
|
+
end
|
54
|
+
|
55
|
+
# If we get here, try one last approach - try to get iDRAC status
|
56
|
+
begin
|
57
|
+
response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1')
|
58
|
+
|
59
|
+
if response.status.between?(200, 299)
|
60
|
+
begin
|
61
|
+
data = JSON.parse(response.body)
|
62
|
+
status = data["Status"] && data["Status"]["State"]
|
63
|
+
if status
|
64
|
+
puts "iDRAC State: #{status}".light_cyan
|
65
|
+
puts "Note: Could not retrieve direct LC status, showing iDRAC status instead".yellow
|
66
|
+
return { "iDRACStatus" => status }
|
67
|
+
end
|
68
|
+
rescue JSON::ParserError
|
69
|
+
# Fall through to final error
|
70
|
+
end
|
71
|
+
end
|
72
|
+
rescue => e
|
73
|
+
# Fall through to final error
|
74
|
+
end
|
75
|
+
|
76
|
+
# If we reached here, all methods failed
|
77
|
+
puts "Unable to retrieve Lifecycle Controller status through any available method".red
|
78
|
+
raise Error, "Failed to get Lifecycle Controller status through any available method"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check if the Lifecycle Controller is enabled
|
82
|
+
def get_idrac_lifecycle_status
|
83
|
+
# Use the DellLCService GetRemoteServicesAPIStatus
|
84
|
+
path = '/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.GetRemoteServicesAPIStatus'
|
85
|
+
|
86
|
+
response = authenticated_request(
|
87
|
+
:post,
|
88
|
+
path,
|
89
|
+
body: {}.to_json,
|
90
|
+
headers: { 'Content-Type' => 'application/json' }
|
91
|
+
)
|
92
|
+
|
93
|
+
if response.status.between?(200, 299)
|
94
|
+
begin
|
95
|
+
lc_data = JSON.parse(response.body)
|
96
|
+
status = lc_data["LCStatus"]
|
97
|
+
|
98
|
+
debug "LC Status: #{status}", 1
|
99
|
+
|
100
|
+
# Get the LCReplication status
|
101
|
+
attributes_path = "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
|
102
|
+
attributes_response = authenticated_request(:get, attributes_path)
|
103
|
+
|
104
|
+
if attributes_response.status == 200
|
105
|
+
begin
|
106
|
+
attributes_data = JSON.parse(attributes_response.body)
|
107
|
+
lc_replication = attributes_data["Attributes"]["ServiceModule.1.LCLReplication"]
|
108
|
+
|
109
|
+
debug "ServiceModule.1.LCLReplication: #{lc_replication}", 1
|
110
|
+
|
111
|
+
is_enabled = lc_replication == "Enabled"
|
112
|
+
|
113
|
+
puts "Lifecycle Controller replication is #{is_enabled ? 'enabled' : 'disabled'}".light_cyan
|
114
|
+
puts "Lifecycle Controller status: #{status}".light_cyan
|
115
|
+
return is_enabled
|
116
|
+
rescue => e
|
117
|
+
debug "Error parsing attributes: #{e.message}", 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# If we can't determine from attributes, just return if LC is Ready
|
122
|
+
is_ready = status == "Ready"
|
123
|
+
puts "Lifecycle Controller is #{is_ready ? 'Ready' : status}".light_cyan
|
124
|
+
return is_ready
|
125
|
+
rescue JSON::ParserError
|
126
|
+
raise Error, "Failed to parse Lifecycle Controller status response: #{response.body}"
|
127
|
+
end
|
128
|
+
else
|
129
|
+
raise Error, "Failed to get Lifecycle Controller status. Status code: #{response.status}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Set the Lifecycle Controller status (enable/disable)
|
134
|
+
def set_idrac_lifecycle_status(status)
|
135
|
+
enabled = !!status # Convert to boolean
|
136
|
+
|
137
|
+
debug "Setting Lifecycle Controller status to #{enabled ? 'enabled' : 'disabled'}", 1
|
138
|
+
|
139
|
+
# Use the attributes method to set the ServiceModule.1.LCLReplication
|
140
|
+
path = "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
|
141
|
+
|
142
|
+
# Create the payload with the attribute we want to modify
|
143
|
+
payload = {
|
144
|
+
"Attributes": {
|
145
|
+
"ServiceModule.1.LCLReplication": enabled ? "Enabled" : "Disabled"
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
debug "Using attributes endpoint: #{path}", 1
|
150
|
+
debug "Payload: #{payload.inspect}", 1
|
151
|
+
|
152
|
+
begin
|
153
|
+
response = authenticated_request(
|
154
|
+
:patch,
|
155
|
+
path,
|
156
|
+
body: payload.to_json,
|
157
|
+
headers: { 'Content-Type' => 'application/json' }
|
158
|
+
)
|
159
|
+
|
160
|
+
debug "Response status: #{response.status}", 1
|
161
|
+
debug "Response body: #{response.body}", 2 if response.body
|
162
|
+
|
163
|
+
if response.status.between?(200, 299)
|
164
|
+
puts "Successfully #{enabled ? 'enabled' : 'disabled'} Lifecycle Controller".green
|
165
|
+
return true
|
166
|
+
else
|
167
|
+
error_message = "Failed to set Lifecycle Controller status. Status code: #{response.status}"
|
168
|
+
|
169
|
+
# Print the full response body for debugging
|
170
|
+
puts "Full error response body:".red
|
171
|
+
puts response.body.inspect.red
|
172
|
+
|
173
|
+
begin
|
174
|
+
error_data = JSON.parse(response.body)
|
175
|
+
puts "Extended error information:".red if error_data['@Message.ExtendedInfo']
|
176
|
+
|
177
|
+
if error_data['error'] && error_data['error']['message']
|
178
|
+
error_message += ", Message: #{error_data['error']['message']}"
|
179
|
+
end
|
180
|
+
|
181
|
+
if error_data['@Message.ExtendedInfo']
|
182
|
+
error_data['@Message.ExtendedInfo'].each do |info|
|
183
|
+
puts " Message: #{info['Message']}".red
|
184
|
+
puts " Resolution: #{info['Resolution']}".yellow if info['Resolution']
|
185
|
+
puts " Severity: #{info['Severity']}".yellow if info['Severity']
|
186
|
+
puts " MessageId: #{info['MessageId']}".yellow if info['MessageId']
|
187
|
+
end
|
188
|
+
|
189
|
+
if error_data['@Message.ExtendedInfo'].first
|
190
|
+
error_message += ", Message: #{error_data['@Message.ExtendedInfo'].first['Message']}"
|
191
|
+
error_message += ", Resolution: #{error_data['@Message.ExtendedInfo'].first['Resolution']}" if error_data['@Message.ExtendedInfo'].first['Resolution']
|
192
|
+
end
|
193
|
+
end
|
194
|
+
rescue => e
|
195
|
+
debug "Error parsing response: #{e.message}", 1
|
196
|
+
# Ignore JSON parsing errors
|
197
|
+
end
|
198
|
+
|
199
|
+
raise Error, error_message
|
200
|
+
end
|
201
|
+
rescue => e
|
202
|
+
debug "Error in request: #{e.message}", 1
|
203
|
+
raise Error, "Failed to set Lifecycle Controller status: #{e.message}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Ensure the Lifecycle Controller is enabled
|
208
|
+
def ensure_lifecycle_controller!
|
209
|
+
if !get_idrac_lifecycle_status
|
210
|
+
puts "Lifecycle Controller is disabled, enabling...".yellow
|
211
|
+
set_idrac_lifecycle_status(true)
|
212
|
+
|
213
|
+
# Verify it was enabled
|
214
|
+
if !get_idrac_lifecycle_status
|
215
|
+
raise Error, "Failed to enable Lifecycle Controller"
|
216
|
+
end
|
217
|
+
|
218
|
+
puts "Lifecycle Controller successfully enabled".green
|
219
|
+
else
|
220
|
+
puts "Lifecycle Controller is already enabled".green
|
221
|
+
end
|
222
|
+
|
223
|
+
return true
|
224
|
+
end
|
225
|
+
|
226
|
+
# Clear the Lifecycle log
|
227
|
+
def clear_lifecycle!
|
228
|
+
path = '/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLCService/Actions/DellLCService.SystemErase'
|
229
|
+
payload = { "Component": ["LCData"] }
|
230
|
+
|
231
|
+
response = authenticated_request(
|
232
|
+
:post,
|
233
|
+
path,
|
234
|
+
body: payload.to_json,
|
235
|
+
headers: { 'Content-Type' => 'application/json' }
|
236
|
+
)
|
237
|
+
|
238
|
+
if response.status.between?(200, 299)
|
239
|
+
puts "Lifecycle log cleared".green
|
240
|
+
return true
|
241
|
+
else
|
242
|
+
puts "Failed to clear Lifecycle log".red
|
243
|
+
|
244
|
+
error_message = "Failed to clear Lifecycle log. Status code: #{response.status}"
|
245
|
+
|
246
|
+
begin
|
247
|
+
error_data = JSON.parse(response.body)
|
248
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
249
|
+
rescue
|
250
|
+
# Ignore JSON parsing errors
|
251
|
+
end
|
252
|
+
|
253
|
+
raise Error, error_message
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Get the system event logs
|
258
|
+
def get_system_event_logs
|
259
|
+
path = 'Managers/iDRAC.Embedded.1/Logs/Sel?$expand=*($levels=1)'
|
260
|
+
|
261
|
+
response = authenticated_request(:get, path)
|
262
|
+
|
263
|
+
if response.status == 200
|
264
|
+
begin
|
265
|
+
logs_data = JSON.parse(response.body)
|
266
|
+
return logs_data
|
267
|
+
rescue JSON::ParserError
|
268
|
+
raise Error, "Failed to parse system event logs response: #{response.body}"
|
269
|
+
end
|
270
|
+
else
|
271
|
+
raise Error, "Failed to get system event logs. Status code: #{response.status}"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Clear the system event logs
|
276
|
+
def clear_system_event_logs!
|
277
|
+
path = 'Managers/iDRAC.Embedded.1/LogServices/Sel/Actions/LogService.ClearLog'
|
278
|
+
|
279
|
+
response = authenticated_request(:post, path, body: {}.to_json, headers: { 'Content-Type' => 'application/json' })
|
280
|
+
|
281
|
+
if response.status.between?(200, 299)
|
282
|
+
puts "System Event Logs cleared".green
|
283
|
+
return true
|
284
|
+
else
|
285
|
+
puts "Failed to clear System Event Logs".red
|
286
|
+
|
287
|
+
error_message = "Failed to clear System Event Logs. Status code: #{response.status}"
|
288
|
+
|
289
|
+
begin
|
290
|
+
error_data = JSON.parse(response.body)
|
291
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
292
|
+
rescue
|
293
|
+
# Ignore JSON parsing errors
|
294
|
+
end
|
295
|
+
|
296
|
+
raise Error, error_message
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
data/lib/idrac/power.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module PowerMethods
|
6
|
+
def power_on(wait: true)
|
7
|
+
# Login to iDRAC if needed
|
8
|
+
login unless @session_id
|
9
|
+
|
10
|
+
puts "Powering on server...".light_cyan
|
11
|
+
|
12
|
+
# Check current power state first
|
13
|
+
current_state = get_power_state rescue "Unknown"
|
14
|
+
if current_state == "On"
|
15
|
+
puts "Server is already powered ON.".yellow
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
# Send power on command (Reset with ResetType=On)
|
20
|
+
path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
|
21
|
+
payload = { "ResetType" => "On" }
|
22
|
+
|
23
|
+
tries = 10
|
24
|
+
while tries > 0
|
25
|
+
response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
|
26
|
+
|
27
|
+
case response.status
|
28
|
+
when 200, 204
|
29
|
+
puts "Server power on command sent successfully".green
|
30
|
+
break
|
31
|
+
when 409
|
32
|
+
begin
|
33
|
+
error_data = JSON.parse(response.body)
|
34
|
+
if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"] &&
|
35
|
+
error_data["error"]["@Message.ExtendedInfo"].any? { |m| m["Message"] =~ /Server is already powered ON/ }
|
36
|
+
puts "Server is already powered ON.".yellow
|
37
|
+
return false
|
38
|
+
else
|
39
|
+
raise Error, "Failed to power on: #{error_data.inspect}"
|
40
|
+
end
|
41
|
+
rescue JSON::ParserError
|
42
|
+
raise Error, "Failed to power on with status 409: #{response.body}"
|
43
|
+
end
|
44
|
+
when 500
|
45
|
+
puts "[iDRAC 500] Server is busy...".red
|
46
|
+
tries -= 1
|
47
|
+
puts "Retrying... #{tries}/10".yellow if tries > 0
|
48
|
+
sleep 10
|
49
|
+
else
|
50
|
+
raise Error, "Unknown response code #{response.status}: #{response.body}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise Error, "Failed to power on after 10 retries" if tries <= 0
|
55
|
+
|
56
|
+
# Wait for power state change if requested
|
57
|
+
wait_for_power_state(target_state: "On", tries: 10) if wait
|
58
|
+
|
59
|
+
return true
|
60
|
+
end
|
61
|
+
|
62
|
+
def power_off(wait: true, kind: "ForceOff")
|
63
|
+
# Login to iDRAC if needed
|
64
|
+
login unless @session_id
|
65
|
+
|
66
|
+
puts "Powering off server...".light_cyan
|
67
|
+
|
68
|
+
# Check current power state first
|
69
|
+
current_state = get_power_state rescue "Unknown"
|
70
|
+
if current_state == "Off"
|
71
|
+
puts "Server is already powered OFF.".yellow
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
|
75
|
+
# Send power off command
|
76
|
+
path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
|
77
|
+
payload = { "ResetType" => kind }
|
78
|
+
|
79
|
+
response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
|
80
|
+
|
81
|
+
case response.status
|
82
|
+
when 200, 204
|
83
|
+
puts "Server power off command sent successfully".green
|
84
|
+
when 409
|
85
|
+
# Conflict -- Server is already off
|
86
|
+
begin
|
87
|
+
error_data = JSON.parse(response.body)
|
88
|
+
if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"] &&
|
89
|
+
error_data["error"]["@Message.ExtendedInfo"].any? { |m| m["Message"] =~ /Server is already powered OFF/ }
|
90
|
+
puts "Server is already powered OFF.".yellow
|
91
|
+
return false
|
92
|
+
else
|
93
|
+
raise Error, "Failed to power off: #{error_data.inspect}"
|
94
|
+
end
|
95
|
+
rescue JSON::ParserError
|
96
|
+
raise Error, "Failed to power off with status 409: #{response.body}"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
error_message = "Failed to power off server. Status code: #{response.status}"
|
100
|
+
begin
|
101
|
+
error_data = JSON.parse(response.body)
|
102
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
103
|
+
rescue
|
104
|
+
# Ignore JSON parsing errors
|
105
|
+
end
|
106
|
+
raise Error, error_message
|
107
|
+
end
|
108
|
+
|
109
|
+
# Wait for power state change if requested
|
110
|
+
if wait
|
111
|
+
success = wait_for_power_state(target_state: "Off", tries: 6)
|
112
|
+
|
113
|
+
# If graceful shutdown failed, try force shutdown
|
114
|
+
if !success && kind != "ForceOff"
|
115
|
+
return power_off(wait: wait, kind: "ForceOff")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
return true
|
120
|
+
end
|
121
|
+
|
122
|
+
def reboot
|
123
|
+
# Login to iDRAC if needed
|
124
|
+
login unless @session_id
|
125
|
+
|
126
|
+
puts "Rebooting server...".light_cyan
|
127
|
+
|
128
|
+
# Send reboot command (Reset with ResetType=ForceRestart)
|
129
|
+
path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
|
130
|
+
payload = { "ResetType" => "ForceRestart" }
|
131
|
+
|
132
|
+
response = authenticated_request(:post, path, body: payload.to_json, headers: { 'Content-Type' => 'application/json' })
|
133
|
+
|
134
|
+
if response.status >= 200 && response.status < 300
|
135
|
+
puts "Server reboot command sent successfully".green
|
136
|
+
return true
|
137
|
+
else
|
138
|
+
error_message = "Failed to reboot server. Status code: #{response.status}"
|
139
|
+
begin
|
140
|
+
error_data = JSON.parse(response.body)
|
141
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
142
|
+
rescue
|
143
|
+
# Ignore JSON parsing errors
|
144
|
+
end
|
145
|
+
|
146
|
+
raise Error, error_message
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def get_power_state
|
151
|
+
# Login to iDRAC if needed
|
152
|
+
login unless @session_id
|
153
|
+
|
154
|
+
# Get system information
|
155
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=PowerState")
|
156
|
+
|
157
|
+
if response.status == 200
|
158
|
+
begin
|
159
|
+
system_data = JSON.parse(response.body)
|
160
|
+
return system_data["PowerState"]
|
161
|
+
rescue JSON::ParserError
|
162
|
+
raise Error, "Failed to parse power state response: #{response.body}"
|
163
|
+
end
|
164
|
+
else
|
165
|
+
raise Error, "Failed to get power state. Status code: #{response.status}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def wait_for_power_state(target_state:, tries: 6)
|
172
|
+
retry_count = tries
|
173
|
+
|
174
|
+
while retry_count > 0
|
175
|
+
begin
|
176
|
+
current_state = get_power_state
|
177
|
+
|
178
|
+
return true if current_state == target_state
|
179
|
+
|
180
|
+
puts "Waiting for power #{target_state == 'On' ? 'on' : 'off'}...".yellow
|
181
|
+
puts "Current state: #{current_state}"
|
182
|
+
retry_count -= 1
|
183
|
+
sleep 8
|
184
|
+
rescue => e
|
185
|
+
puts "Error checking power state: #{e.message}".red
|
186
|
+
retry_count -= 1
|
187
|
+
sleep 5
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
puts "Failed to reach power state #{target_state}".red
|
192
|
+
return false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|