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
data/lib/idrac/client.rb
CHANGED
@@ -10,9 +10,15 @@ require 'colorize'
|
|
10
10
|
module IDRAC
|
11
11
|
class Client
|
12
12
|
attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :auto_delete_sessions, :session, :web
|
13
|
-
attr_accessor :direct_mode
|
13
|
+
attr_accessor :direct_mode, :verbosity
|
14
|
+
|
15
|
+
include PowerMethods
|
16
|
+
include SessionMethods
|
17
|
+
include Debuggable
|
18
|
+
include JobMethods
|
19
|
+
include LifecycleMethods
|
14
20
|
|
15
|
-
def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl:
|
21
|
+
def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, auto_delete_sessions: true)
|
16
22
|
@host = host
|
17
23
|
@username = username
|
18
24
|
@password = password
|
@@ -21,6 +27,7 @@ module IDRAC
|
|
21
27
|
@verify_ssl = verify_ssl
|
22
28
|
@direct_mode = direct_mode
|
23
29
|
@auto_delete_sessions = auto_delete_sessions
|
30
|
+
@verbosity = 0
|
24
31
|
|
25
32
|
# Initialize the session and web classes
|
26
33
|
@session = Session.new(self)
|
@@ -32,6 +39,13 @@ module IDRAC
|
|
32
39
|
faraday.request :multipart
|
33
40
|
faraday.request :url_encoded
|
34
41
|
faraday.adapter Faraday.default_adapter
|
42
|
+
# Add request/response logging based on verbosity
|
43
|
+
if @verbosity > 0
|
44
|
+
faraday.response :logger, Logger.new(STDOUT), bodies: @verbosity >= 2 do |logger|
|
45
|
+
logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
|
46
|
+
logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
|
47
|
+
end
|
48
|
+
end
|
35
49
|
end
|
36
50
|
end
|
37
51
|
|
@@ -70,6 +84,8 @@ module IDRAC
|
|
70
84
|
raise Error, "Maximum retry count reached for authenticated request"
|
71
85
|
end
|
72
86
|
|
87
|
+
debug "Authenticated request: #{method.to_s.upcase} #{path}", 1
|
88
|
+
|
73
89
|
# If we're in direct mode, use Basic Auth
|
74
90
|
if @direct_mode
|
75
91
|
# Create Basic Auth header
|
@@ -79,6 +95,8 @@ module IDRAC
|
|
79
95
|
options[:headers] ||= {}
|
80
96
|
options[:headers]['Authorization'] = auth_header
|
81
97
|
|
98
|
+
debug "Using Basic Auth for request", 2
|
99
|
+
|
82
100
|
# Make the request
|
83
101
|
begin
|
84
102
|
response = connection.send(method, path) do |req|
|
@@ -86,6 +104,10 @@ module IDRAC
|
|
86
104
|
req.body = options[:body] if options[:body]
|
87
105
|
end
|
88
106
|
|
107
|
+
debug "Response status: #{response.status}", 1
|
108
|
+
debug "Response headers: #{response.headers.inspect}", 2
|
109
|
+
debug "Response body: #{response.body}", 3 if response.body
|
110
|
+
|
89
111
|
return response
|
90
112
|
rescue => e
|
91
113
|
puts "Error during authenticated request (direct mode): #{e.message}".red.bold
|
@@ -98,6 +120,8 @@ module IDRAC
|
|
98
120
|
options[:headers] ||= {}
|
99
121
|
options[:headers]['X-Auth-Token'] = session.x_auth_token
|
100
122
|
|
123
|
+
debug "Using X-Auth-Token for request", 2
|
124
|
+
|
101
125
|
# Make the request
|
102
126
|
begin
|
103
127
|
response = connection.send(method, path) do |req|
|
@@ -105,6 +129,10 @@ module IDRAC
|
|
105
129
|
req.body = options[:body] if options[:body]
|
106
130
|
end
|
107
131
|
|
132
|
+
debug "Response status: #{response.status}", 1
|
133
|
+
debug "Response headers: #{response.headers.inspect}", 2
|
134
|
+
debug "Response body: #{response.body}", 3 if response.body
|
135
|
+
|
108
136
|
# Check if the session is still valid
|
109
137
|
if response.status == 401 || response.status == 403
|
110
138
|
puts "Session expired or invalid, attempting to create a new session...".light_yellow
|
@@ -154,6 +182,8 @@ module IDRAC
|
|
154
182
|
web.login unless web.session_id
|
155
183
|
end
|
156
184
|
|
185
|
+
debug "GET request to #{base_url}/#{path}", 1
|
186
|
+
|
157
187
|
headers_to_use = {
|
158
188
|
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
|
159
189
|
"Accept-Encoding" => "deflate, gzip"
|
@@ -161,18 +191,29 @@ module IDRAC
|
|
161
191
|
|
162
192
|
if web.cookies
|
163
193
|
headers_to_use["Cookie"] = web.cookies
|
194
|
+
debug "Using WebUI cookies for request", 2
|
164
195
|
elsif @direct_mode
|
165
196
|
# In direct mode, use Basic Auth
|
166
197
|
headers_to_use["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
198
|
+
debug "Using Basic Auth for GET request", 2
|
167
199
|
elsif session.x_auth_token
|
168
200
|
headers_to_use["X-Auth-Token"] = session.x_auth_token
|
201
|
+
debug "Using X-Auth-Token for GET request", 2
|
169
202
|
end
|
170
203
|
|
171
|
-
|
204
|
+
debug "Request headers: #{headers_to_use.merge(headers).inspect}", 3
|
205
|
+
|
206
|
+
response = HTTParty.get(
|
172
207
|
"#{base_url}/#{path}",
|
173
208
|
headers: headers_to_use.merge(headers),
|
174
209
|
verify: false
|
175
210
|
)
|
211
|
+
|
212
|
+
debug "Response status: #{response.code}", 1
|
213
|
+
debug "Response headers: #{response.headers.inspect}", 2
|
214
|
+
debug "Response body: #{response.body.to_s[0..500]}#{response.body.to_s.length > 500 ? '...' : ''}", 3 if response.body
|
215
|
+
|
216
|
+
response
|
176
217
|
end
|
177
218
|
|
178
219
|
def screenshot
|
@@ -183,5 +224,15 @@ module IDRAC
|
|
183
224
|
protocol = use_ssl ? 'https' : 'http'
|
184
225
|
"#{protocol}://#{host}:#{port}"
|
185
226
|
end
|
227
|
+
|
228
|
+
def redfish_version
|
229
|
+
response = authenticated_request(:get, "/redfish/v1")
|
230
|
+
if response.status == 200
|
231
|
+
data = JSON.parse(response.body)
|
232
|
+
data["RedfishVersion"]
|
233
|
+
else
|
234
|
+
raise Error, "Failed to get Redfish version: #{response.status} - #{response.body}"
|
235
|
+
end
|
236
|
+
end
|
186
237
|
end
|
187
238
|
end
|
data/lib/idrac/firmware.rb
CHANGED
@@ -8,6 +8,8 @@ require 'securerandom'
|
|
8
8
|
require 'set'
|
9
9
|
require 'colorize'
|
10
10
|
require_relative 'firmware_catalog'
|
11
|
+
require 'faraday'
|
12
|
+
require 'faraday/multipart'
|
11
13
|
|
12
14
|
module IDRAC
|
13
15
|
class Firmware
|
@@ -427,6 +429,24 @@ module IDRAC
|
|
427
429
|
end
|
428
430
|
end
|
429
431
|
|
432
|
+
def get_power_state
|
433
|
+
# Ensure we have a client
|
434
|
+
raise Error, "Client is required for power management" unless client
|
435
|
+
|
436
|
+
# Login to iDRAC if needed
|
437
|
+
client.login unless client.instance_variable_get(:@session_id)
|
438
|
+
|
439
|
+
# Get system information
|
440
|
+
response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
441
|
+
|
442
|
+
if response.status == 200
|
443
|
+
system_data = JSON.parse(response.body)
|
444
|
+
return system_data["PowerState"]
|
445
|
+
else
|
446
|
+
raise Error, "Failed to get power state. Status code: #{response.status}"
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
430
450
|
private
|
431
451
|
|
432
452
|
def upload_firmware(firmware_path)
|
@@ -478,16 +498,20 @@ module IDRAC
|
|
478
498
|
file_content = File.read(firmware_path)
|
479
499
|
|
480
500
|
headers = {
|
481
|
-
'Content-Type' => '
|
501
|
+
'Content-Type' => 'multipart/form-data',
|
482
502
|
'If-Match' => etag
|
483
503
|
}
|
484
504
|
|
505
|
+
# Create a temp file for multipart upload
|
506
|
+
upload_io = Faraday::UploadIO.new(firmware_path, 'application/octet-stream')
|
507
|
+
payload = { :file => upload_io }
|
508
|
+
|
485
509
|
upload_response = client.authenticated_request(
|
486
510
|
:post,
|
487
511
|
http_push_uri,
|
488
512
|
{
|
489
513
|
headers: headers,
|
490
|
-
body:
|
514
|
+
body: payload,
|
491
515
|
}
|
492
516
|
)
|
493
517
|
|
data/lib/idrac/jobs.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module JobMethods
|
6
|
+
# Get a list of jobs
|
7
|
+
def jobs
|
8
|
+
response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)')
|
9
|
+
|
10
|
+
if response.status == 200
|
11
|
+
begin
|
12
|
+
jobs_data = JSON.parse(response.body)
|
13
|
+
puts "Jobs: #{jobs_data['Members'].count}"
|
14
|
+
if jobs_data['Members'].count > 0
|
15
|
+
puts "Job IDs:"
|
16
|
+
jobs_data["Members"].each do |job|
|
17
|
+
puts " #{job['Id']}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
return jobs_data
|
21
|
+
rescue JSON::ParserError
|
22
|
+
raise Error, "Failed to parse jobs response: #{response.body}"
|
23
|
+
end
|
24
|
+
else
|
25
|
+
raise Error, "Failed to get jobs. Status code: #{response.status}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get detailed job information
|
30
|
+
def jobs_detail
|
31
|
+
response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)')
|
32
|
+
|
33
|
+
if response.status == 200
|
34
|
+
begin
|
35
|
+
jobs_data = JSON.parse(response.body)
|
36
|
+
jobs_data["Members"].each do |job|
|
37
|
+
puts "#{job['Id']} : #{job['JobState']} > #{job['Message']}"
|
38
|
+
end
|
39
|
+
return jobs_data
|
40
|
+
rescue JSON::ParserError
|
41
|
+
raise Error, "Failed to parse jobs detail response: #{response.body}"
|
42
|
+
end
|
43
|
+
else
|
44
|
+
raise Error, "Failed to get jobs detail. Status code: #{response.status}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clear all jobs from the job queue
|
49
|
+
def clear_jobs!
|
50
|
+
# Get list of jobs
|
51
|
+
jobs_response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)')
|
52
|
+
|
53
|
+
if jobs_response.status == 200
|
54
|
+
begin
|
55
|
+
jobs_data = JSON.parse(jobs_response.body)
|
56
|
+
members = jobs_data["Members"]
|
57
|
+
|
58
|
+
# Delete each job individually
|
59
|
+
members.each.with_index do |job, i|
|
60
|
+
puts "Removing #{job['Id']} [#{i+1}/#{members.count}]"
|
61
|
+
delete_response = authenticated_request(:delete, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{job['Id']}")
|
62
|
+
|
63
|
+
unless delete_response.status.between?(200, 299)
|
64
|
+
puts "Warning: Failed to delete job #{job['Id']}. Status code: #{delete_response.status}".yellow
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
puts "Successfully cleared all jobs".green
|
69
|
+
return true
|
70
|
+
rescue JSON::ParserError
|
71
|
+
raise Error, "Failed to parse jobs response: #{jobs_response.body}"
|
72
|
+
end
|
73
|
+
else
|
74
|
+
raise Error, "Failed to get jobs. Status code: #{jobs_response.status}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Force clear the job queue
|
79
|
+
def force_clear_jobs!
|
80
|
+
# Clear the job queue using force option which will also clear any pending data and restart processes
|
81
|
+
path = '/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellJobService/Actions/DellJobService.DeleteJobQueue'
|
82
|
+
payload = { "JobID" => "JID_CLEARALL_FORCE" }
|
83
|
+
|
84
|
+
response = authenticated_request(
|
85
|
+
:post,
|
86
|
+
path,
|
87
|
+
body: payload.to_json,
|
88
|
+
headers: { 'Content-Type' => 'application/json' }
|
89
|
+
)
|
90
|
+
|
91
|
+
if response.status.between?(200, 299)
|
92
|
+
puts "Successfully force-cleared job queue".green
|
93
|
+
|
94
|
+
# Monitor LC status until it's Ready
|
95
|
+
puts "Waiting for LC status to be Ready..."
|
96
|
+
|
97
|
+
retries = 12 # ~2 minutes with 10s sleep
|
98
|
+
while retries > 0
|
99
|
+
lc_response = authenticated_request(
|
100
|
+
:post,
|
101
|
+
'/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.GetRemoteServicesAPIStatus',
|
102
|
+
body: {}.to_json,
|
103
|
+
headers: { 'Content-Type': 'application/json' }
|
104
|
+
)
|
105
|
+
|
106
|
+
if lc_response.status.between?(200, 299)
|
107
|
+
begin
|
108
|
+
lc_data = JSON.parse(lc_response.body)
|
109
|
+
status = lc_data["LCStatus"]
|
110
|
+
|
111
|
+
if status == "Ready"
|
112
|
+
puts "LC Status is Ready".green
|
113
|
+
return true
|
114
|
+
end
|
115
|
+
|
116
|
+
puts "Current LC Status: #{status}. Waiting..."
|
117
|
+
rescue JSON::ParserError
|
118
|
+
puts "Failed to parse LC status response, will retry...".yellow
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
retries -= 1
|
123
|
+
sleep 10
|
124
|
+
end
|
125
|
+
|
126
|
+
puts "Warning: LC status did not reach Ready state within timeout".yellow
|
127
|
+
return true
|
128
|
+
else
|
129
|
+
error_message = "Failed to force-clear job queue. Status code: #{response.status}"
|
130
|
+
|
131
|
+
begin
|
132
|
+
error_data = JSON.parse(response.body)
|
133
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
134
|
+
rescue
|
135
|
+
# Ignore JSON parsing errors
|
136
|
+
end
|
137
|
+
|
138
|
+
raise Error, error_message
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Wait for a job to complete
|
143
|
+
def wait_for_job(job_id)
|
144
|
+
# Job ID can be a job ID, path, or response hash from another request
|
145
|
+
job_path = if job_id.is_a?(Hash)
|
146
|
+
if job_id['headers'] && job_id['headers']['location']
|
147
|
+
job_id['headers']['location'].sub(/^\/redfish\/v1\//, '')
|
148
|
+
else
|
149
|
+
raise Error, "Invalid job hash, missing location header"
|
150
|
+
end
|
151
|
+
elsif job_id.to_s.start_with?('/redfish/v1/')
|
152
|
+
job_id.sub(/^\/redfish\/v1\//, '')
|
153
|
+
else
|
154
|
+
"Managers/iDRAC.Embedded.1/Jobs/#{job_id}"
|
155
|
+
end
|
156
|
+
|
157
|
+
puts "Waiting for job to complete: #{job_id}".light_cyan
|
158
|
+
|
159
|
+
retries = 36 # ~6 minutes with 10s sleep
|
160
|
+
while retries > 0
|
161
|
+
response = authenticated_request(:get, "/redfish/v1/#{job_path}")
|
162
|
+
|
163
|
+
if response.status == 200
|
164
|
+
begin
|
165
|
+
job_data = JSON.parse(response.body)
|
166
|
+
job_state = job_data["JobState"]
|
167
|
+
|
168
|
+
case job_state
|
169
|
+
when "Completed"
|
170
|
+
puts "Job completed successfully".green
|
171
|
+
return job_data
|
172
|
+
when "Failed"
|
173
|
+
puts "Job failed: #{job_data['Message']}".red
|
174
|
+
raise Error, "Job failed: #{job_data['Message']}"
|
175
|
+
when "CompletedWithErrors"
|
176
|
+
puts "Job completed with errors: #{job_data['Message']}".yellow
|
177
|
+
return job_data
|
178
|
+
end
|
179
|
+
|
180
|
+
puts "Job state: #{job_state}. Waiting...".yellow
|
181
|
+
rescue JSON::ParserError
|
182
|
+
puts "Failed to parse job status response, will retry...".yellow
|
183
|
+
end
|
184
|
+
else
|
185
|
+
puts "Failed to get job status. Status code: #{response.status}".red
|
186
|
+
end
|
187
|
+
|
188
|
+
retries -= 1
|
189
|
+
sleep 10
|
190
|
+
end
|
191
|
+
|
192
|
+
raise Error, "Timeout waiting for job to complete"
|
193
|
+
end
|
194
|
+
|
195
|
+
# Get system tasks
|
196
|
+
def tasks
|
197
|
+
response = authenticated_request(:get, '/redfish/v1/TaskService/Tasks')
|
198
|
+
|
199
|
+
if response.status == 200
|
200
|
+
begin
|
201
|
+
tasks_data = JSON.parse(response.body)
|
202
|
+
# "Tasks: #{tasks_data['Members'].count}", 0
|
203
|
+
return tasks_data['Members']
|
204
|
+
rescue JSON::ParserError
|
205
|
+
raise Error, "Failed to parse tasks response: #{response.body}"
|
206
|
+
end
|
207
|
+
else
|
208
|
+
raise Error, "Failed to get tasks. Status code: #{response.status}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|