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.
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: true, direct_mode: false, auto_delete_sessions: true)
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
- HTTParty.get(
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
@@ -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' => 'application/octet-stream',
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: file_content
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