idrac 0.1.31 → 0.1.38
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 +48 -101
- data/bin/idrac +128 -12
- data/idrac.gemspec +9 -8
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac.rb +1 -0
- metadata +3 -36
- data/.rspec +0 -3
- data/Rakefile +0 -17
- data/dell_firmware_downloads/Catalog.etag +0 -1
- data/dell_firmware_downloads/Catalog.xml +0 -0
- data/idrac-0.1.6/.rspec +0 -3
- data/idrac-0.1.6/README.md +0 -103
- data/idrac-0.1.6/Rakefile +0 -17
- data/idrac-0.1.6/bin/console +0 -11
- data/idrac-0.1.6/bin/idrac +0 -179
- data/idrac-0.1.6/bin/setup +0 -8
- data/idrac-0.1.6/idrac.gemspec +0 -51
- data/idrac-0.1.6/lib/idrac/client.rb +0 -109
- data/idrac-0.1.6/lib/idrac/firmware.rb +0 -366
- data/idrac-0.1.6/lib/idrac/version.rb +0 -5
- data/idrac-0.1.6/lib/idrac.rb +0 -30
- data/idrac-0.1.6/sig/idrac.rbs +0 -4
- data/idrac-0.1.7/.rspec +0 -3
- data/idrac-0.1.7/README.md +0 -103
- data/idrac-0.1.7/Rakefile +0 -17
- data/idrac-0.1.7/bin/console +0 -11
- data/idrac-0.1.7/bin/idrac +0 -179
- data/idrac-0.1.7/bin/setup +0 -8
- data/idrac-0.1.7/idrac.gemspec +0 -51
- data/idrac-0.1.7/lib/idrac/client.rb +0 -109
- data/idrac-0.1.7/lib/idrac/firmware.rb +0 -366
- data/idrac-0.1.7/lib/idrac/screenshot.rb +0 -49
- data/idrac-0.1.7/lib/idrac/version.rb +0 -5
- data/idrac-0.1.7/lib/idrac.rb +0 -30
- data/idrac-0.1.7/sig/idrac.rbs +0 -4
- data/idrac.py +0 -500
- data/sig/idrac.rbs +0 -4
- data/test_firmware_update.rb +0 -68
- data/updater.rb +0 -729
@@ -1,366 +0,0 @@
|
|
1
|
-
require 'tempfile'
|
2
|
-
require 'net/http'
|
3
|
-
require 'uri'
|
4
|
-
require 'json'
|
5
|
-
require 'nokogiri'
|
6
|
-
require 'fileutils'
|
7
|
-
require 'securerandom'
|
8
|
-
|
9
|
-
module IDRAC
|
10
|
-
class Firmware
|
11
|
-
attr_reader :client
|
12
|
-
|
13
|
-
CATALOG_URL = "https://downloads.dell.com/catalog/Catalog.xml.gz"
|
14
|
-
|
15
|
-
def initialize(client)
|
16
|
-
@client = client
|
17
|
-
end
|
18
|
-
|
19
|
-
def update(firmware_path, options = {})
|
20
|
-
# Validate firmware file exists
|
21
|
-
unless File.exist?(firmware_path)
|
22
|
-
raise Error, "Firmware file not found: #{firmware_path}"
|
23
|
-
end
|
24
|
-
|
25
|
-
# Login to iDRAC
|
26
|
-
client.login unless client.instance_variable_get(:@session_id)
|
27
|
-
|
28
|
-
# Upload firmware file
|
29
|
-
job_id = upload_firmware(firmware_path)
|
30
|
-
|
31
|
-
# Check if we should wait for the update to complete
|
32
|
-
if options[:wait]
|
33
|
-
wait_for_job_completion(job_id, options[:timeout] || 3600)
|
34
|
-
end
|
35
|
-
|
36
|
-
job_id
|
37
|
-
end
|
38
|
-
|
39
|
-
def download_catalog(output_dir = nil)
|
40
|
-
output_dir ||= Dir.pwd
|
41
|
-
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
42
|
-
|
43
|
-
catalog_gz_path = File.join(output_dir, "Catalog.xml.gz")
|
44
|
-
catalog_path = File.join(output_dir, "Catalog.xml")
|
45
|
-
|
46
|
-
puts "Downloading Dell catalog from #{CATALOG_URL}..."
|
47
|
-
|
48
|
-
uri = URI.parse(CATALOG_URL)
|
49
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
50
|
-
request = Net::HTTP::Get.new(uri)
|
51
|
-
http.request(request) do |response|
|
52
|
-
if response.code == "200"
|
53
|
-
File.open(catalog_gz_path, 'wb') do |file|
|
54
|
-
response.read_body do |chunk|
|
55
|
-
file.write(chunk)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
else
|
59
|
-
raise Error, "Failed to download catalog: #{response.code} #{response.message}"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
puts "Extracting catalog..."
|
65
|
-
system("gunzip -f #{catalog_gz_path}")
|
66
|
-
|
67
|
-
if File.exist?(catalog_path)
|
68
|
-
puts "Catalog downloaded and extracted to #{catalog_path}"
|
69
|
-
return catalog_path
|
70
|
-
else
|
71
|
-
raise Error, "Failed to extract catalog"
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def get_system_inventory
|
76
|
-
puts "Retrieving system inventory..."
|
77
|
-
|
78
|
-
# Get basic system information
|
79
|
-
system_uri = URI.parse("#{client.base_url}/redfish/v1/Systems/System.Embedded.1")
|
80
|
-
system_response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
81
|
-
|
82
|
-
if system_response.status != 200
|
83
|
-
raise Error, "Failed to get system information: #{system_response.status}"
|
84
|
-
end
|
85
|
-
|
86
|
-
system_data = JSON.parse(system_response.body)
|
87
|
-
|
88
|
-
# Get firmware inventory
|
89
|
-
firmware_uri = URI.parse("#{client.base_url}/redfish/v1/UpdateService/FirmwareInventory")
|
90
|
-
firmware_response = client.authenticated_request(:get, "/redfish/v1/UpdateService/FirmwareInventory")
|
91
|
-
|
92
|
-
if firmware_response.status != 200
|
93
|
-
raise Error, "Failed to get firmware inventory: #{firmware_response.status}"
|
94
|
-
end
|
95
|
-
|
96
|
-
firmware_data = JSON.parse(firmware_response.body)
|
97
|
-
|
98
|
-
# Get detailed firmware information for each component
|
99
|
-
firmware_inventory = []
|
100
|
-
|
101
|
-
if firmware_data['Members'] && firmware_data['Members'].is_a?(Array)
|
102
|
-
firmware_data['Members'].each do |member|
|
103
|
-
if member['@odata.id']
|
104
|
-
component_uri = member['@odata.id']
|
105
|
-
component_response = client.authenticated_request(:get, component_uri)
|
106
|
-
|
107
|
-
if component_response.status == 200
|
108
|
-
component_data = JSON.parse(component_response.body)
|
109
|
-
firmware_inventory << {
|
110
|
-
name: component_data['Name'],
|
111
|
-
id: component_data['Id'],
|
112
|
-
version: component_data['Version'],
|
113
|
-
updateable: component_data['Updateable'] || false,
|
114
|
-
status: component_data['Status'] ? component_data['Status']['State'] : 'Unknown'
|
115
|
-
}
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
{
|
122
|
-
system: {
|
123
|
-
model: system_data['Model'],
|
124
|
-
manufacturer: system_data['Manufacturer'],
|
125
|
-
serial_number: system_data['SerialNumber'],
|
126
|
-
part_number: system_data['PartNumber'],
|
127
|
-
bios_version: system_data['BiosVersion'],
|
128
|
-
service_tag: system_data['SKU']
|
129
|
-
},
|
130
|
-
firmware: firmware_inventory
|
131
|
-
}
|
132
|
-
end
|
133
|
-
|
134
|
-
def check_updates(catalog_path = nil)
|
135
|
-
# Download catalog if not provided
|
136
|
-
catalog_path ||= download_catalog
|
137
|
-
|
138
|
-
# Get system inventory
|
139
|
-
inventory = get_system_inventory
|
140
|
-
|
141
|
-
# Parse catalog
|
142
|
-
catalog_doc = File.open(catalog_path) { |f| Nokogiri::XML(f) }
|
143
|
-
|
144
|
-
# Extract service tag
|
145
|
-
service_tag = inventory[:system][:service_tag]
|
146
|
-
|
147
|
-
puts "Checking updates for system with service tag: #{service_tag}"
|
148
|
-
|
149
|
-
# Find applicable updates
|
150
|
-
updates = []
|
151
|
-
|
152
|
-
# Get current firmware versions
|
153
|
-
current_versions = {}
|
154
|
-
inventory[:firmware].each do |fw|
|
155
|
-
current_versions[fw[:name]] = fw[:version]
|
156
|
-
end
|
157
|
-
|
158
|
-
# Find matching components in catalog
|
159
|
-
catalog_doc.xpath('//SoftwareComponent').each do |component|
|
160
|
-
name = component.at_xpath('Name')&.text
|
161
|
-
version = component.at_xpath('Version')&.text
|
162
|
-
path = component.at_xpath('Path')&.text
|
163
|
-
component_type = component.at_xpath('ComponentType')&.text
|
164
|
-
|
165
|
-
# Check if this component matches any of our firmware
|
166
|
-
inventory[:firmware].each do |fw|
|
167
|
-
if fw[:name].include?(name) || name.include?(fw[:name])
|
168
|
-
current_version = fw[:version]
|
169
|
-
|
170
|
-
# Simple version comparison (this could be improved)
|
171
|
-
if version != current_version
|
172
|
-
updates << {
|
173
|
-
name: name,
|
174
|
-
current_version: current_version,
|
175
|
-
available_version: version,
|
176
|
-
path: path,
|
177
|
-
component_type: component_type,
|
178
|
-
download_url: "https://downloads.dell.com/#{path}"
|
179
|
-
}
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
updates
|
186
|
-
end
|
187
|
-
|
188
|
-
def interactive_update(catalog_path = nil)
|
189
|
-
updates = check_updates(catalog_path)
|
190
|
-
|
191
|
-
if updates.empty?
|
192
|
-
puts "No updates available for your system."
|
193
|
-
return
|
194
|
-
end
|
195
|
-
|
196
|
-
puts "\nAvailable updates:"
|
197
|
-
updates.each_with_index do |update, index|
|
198
|
-
puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
|
199
|
-
end
|
200
|
-
|
201
|
-
puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):"
|
202
|
-
choice = STDIN.gets.chomp
|
203
|
-
|
204
|
-
return if choice.downcase == 'q'
|
205
|
-
|
206
|
-
selected_updates = if choice.downcase == 'all'
|
207
|
-
updates
|
208
|
-
else
|
209
|
-
index = choice.to_i - 1
|
210
|
-
if index >= 0 && index < updates.size
|
211
|
-
[updates[index]]
|
212
|
-
else
|
213
|
-
puts "Invalid selection."
|
214
|
-
return
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
selected_updates.each do |update|
|
219
|
-
puts "Downloading #{update[:name]} version #{update[:available_version]}..."
|
220
|
-
|
221
|
-
# Create temp directory
|
222
|
-
temp_dir = Dir.mktmpdir
|
223
|
-
|
224
|
-
begin
|
225
|
-
# Download the update
|
226
|
-
update_filename = File.basename(update[:path])
|
227
|
-
update_path = File.join(temp_dir, update_filename)
|
228
|
-
|
229
|
-
uri = URI.parse(update[:download_url])
|
230
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
231
|
-
request = Net::HTTP::Get.new(uri)
|
232
|
-
http.request(request) do |response|
|
233
|
-
if response.code == "200"
|
234
|
-
File.open(update_path, 'wb') do |file|
|
235
|
-
response.read_body do |chunk|
|
236
|
-
file.write(chunk)
|
237
|
-
end
|
238
|
-
end
|
239
|
-
else
|
240
|
-
puts "Failed to download update: #{response.code} #{response.message}"
|
241
|
-
next
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
puts "Installing #{update[:name]} version #{update[:available_version]}..."
|
247
|
-
job_id = update(update_path, wait: true)
|
248
|
-
puts "Update completed with job ID: #{job_id}"
|
249
|
-
|
250
|
-
ensure
|
251
|
-
# Clean up temp directory
|
252
|
-
FileUtils.remove_entry(temp_dir)
|
253
|
-
end
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
private
|
258
|
-
|
259
|
-
def upload_firmware(firmware_path)
|
260
|
-
puts "Uploading firmware file: #{firmware_path}"
|
261
|
-
|
262
|
-
# Get the HttpPushUri from UpdateService
|
263
|
-
update_service_response = client.authenticated_request(:get, "/redfish/v1/UpdateService")
|
264
|
-
update_service_data = JSON.parse(update_service_response.body)
|
265
|
-
|
266
|
-
http_push_uri = update_service_data['HttpPushUri']
|
267
|
-
if http_push_uri.nil?
|
268
|
-
http_push_uri = "/redfish/v1/UpdateService/FirmwareInventory"
|
269
|
-
puts "HttpPushUri not found, using default: #{http_push_uri}"
|
270
|
-
else
|
271
|
-
puts "Found HttpPushUri: #{http_push_uri}"
|
272
|
-
end
|
273
|
-
|
274
|
-
# Get the ETag for the HttpPushUri
|
275
|
-
etag_response = client.authenticated_request(:get, http_push_uri)
|
276
|
-
etag = etag_response.headers['etag']
|
277
|
-
|
278
|
-
puts "Got ETag: #{etag}"
|
279
|
-
|
280
|
-
# Create a boundary for multipart/form-data
|
281
|
-
boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
|
282
|
-
|
283
|
-
# Read the file content
|
284
|
-
file_content = File.binread(firmware_path)
|
285
|
-
filename = File.basename(firmware_path)
|
286
|
-
|
287
|
-
# Create the multipart body
|
288
|
-
post_body = []
|
289
|
-
post_body << "--#{boundary}\r\n"
|
290
|
-
post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
|
291
|
-
post_body << "Content-Type: application/octet-stream\r\n\r\n"
|
292
|
-
post_body << file_content
|
293
|
-
post_body << "\r\n--#{boundary}--\r\n"
|
294
|
-
|
295
|
-
# Upload the firmware
|
296
|
-
response = client.authenticated_request(
|
297
|
-
:post,
|
298
|
-
http_push_uri,
|
299
|
-
{
|
300
|
-
headers: {
|
301
|
-
'Content-Type' => "multipart/form-data; boundary=#{boundary}",
|
302
|
-
'If-Match' => etag
|
303
|
-
},
|
304
|
-
body: post_body.join
|
305
|
-
}
|
306
|
-
)
|
307
|
-
|
308
|
-
if response.status < 200 || response.status >= 300
|
309
|
-
raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
|
310
|
-
end
|
311
|
-
|
312
|
-
# Extract job ID from response
|
313
|
-
response_data = JSON.parse(response.body)
|
314
|
-
job_id = response_data['Id'] || response_data['TaskId']
|
315
|
-
|
316
|
-
if job_id.nil?
|
317
|
-
raise Error, "Failed to extract job ID from firmware upload response"
|
318
|
-
end
|
319
|
-
|
320
|
-
puts "Firmware update job created with ID: #{job_id}"
|
321
|
-
job_id
|
322
|
-
end
|
323
|
-
|
324
|
-
def wait_for_job_completion(job_id, timeout)
|
325
|
-
puts "Waiting for firmware update job #{job_id} to complete..."
|
326
|
-
|
327
|
-
start_time = Time.now
|
328
|
-
loop do
|
329
|
-
status = get_job_status(job_id)
|
330
|
-
|
331
|
-
case status
|
332
|
-
when 'Completed'
|
333
|
-
puts "Firmware update completed successfully"
|
334
|
-
return true
|
335
|
-
when 'Failed'
|
336
|
-
raise Error, "Firmware update job failed"
|
337
|
-
when 'Scheduled', 'Running', 'Downloading', 'Pending'
|
338
|
-
# Job still in progress
|
339
|
-
else
|
340
|
-
puts "Unknown job status: #{status}"
|
341
|
-
end
|
342
|
-
|
343
|
-
if Time.now - start_time > timeout
|
344
|
-
raise Error, "Firmware update timed out after #{timeout} seconds"
|
345
|
-
end
|
346
|
-
|
347
|
-
# Wait before checking again
|
348
|
-
sleep 10
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
def get_job_status(job_id)
|
353
|
-
response = client.authenticated_request(
|
354
|
-
:get,
|
355
|
-
"/redfish/v1/TaskService/Tasks/#{job_id}"
|
356
|
-
)
|
357
|
-
|
358
|
-
if response.status != 200
|
359
|
-
raise Error, "Failed to get job status with status #{response.status}: #{response.body}"
|
360
|
-
end
|
361
|
-
|
362
|
-
response_data = JSON.parse(response.body)
|
363
|
-
response_data['TaskState'] || 'Unknown'
|
364
|
-
end
|
365
|
-
end
|
366
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
require 'httparty'
|
2
|
-
require 'nokogiri'
|
3
|
-
|
4
|
-
module IDRAC
|
5
|
-
# Reverse engineered screenshot functionality for iDRAC
|
6
|
-
# This uses introspection on how the web UI creates screenshots rather than the Redfish API
|
7
|
-
class Screenshot
|
8
|
-
attr_reader :client
|
9
|
-
|
10
|
-
def initialize(client)
|
11
|
-
@client = client
|
12
|
-
end
|
13
|
-
|
14
|
-
def capture
|
15
|
-
# Login to get the forward URL and cookies
|
16
|
-
forward_url = client.login
|
17
|
-
|
18
|
-
# Extract the key-value pairs from the forward URL (format: index?ST1=ABC,ST2=DEF)
|
19
|
-
tokens = forward_url.split("?").last.split(",").inject({}) do |acc, kv|
|
20
|
-
k, v = kv.split("=")
|
21
|
-
acc[k] = v
|
22
|
-
acc
|
23
|
-
end
|
24
|
-
|
25
|
-
# Generate a timestamp for the request
|
26
|
-
timestamp_ms = (Time.now.to_f * 1000).to_i
|
27
|
-
|
28
|
-
# First request to trigger the screenshot capture
|
29
|
-
path = "data?get=consolepreview[manual%20#{timestamp_ms}]"
|
30
|
-
res = client.get(path: path, headers: tokens)
|
31
|
-
raise Error, "Failed to trigger screenshot capture." unless res.code.between?(200, 299)
|
32
|
-
|
33
|
-
# Wait for the screenshot to be generated
|
34
|
-
sleep 2
|
35
|
-
|
36
|
-
# Second request to get the actual screenshot image
|
37
|
-
path = "capconsole/scapture0.png?#{timestamp_ms}"
|
38
|
-
res = client.get(path: path, headers: tokens)
|
39
|
-
raise Error, "Failed to retrieve screenshot image." unless res.code.between?(200, 299)
|
40
|
-
|
41
|
-
# Save the screenshot to a file
|
42
|
-
filename = "idrac_screenshot_#{timestamp_ms}.png"
|
43
|
-
File.open(filename, "wb") { |f| f.write(res.body) }
|
44
|
-
|
45
|
-
# Return the filename
|
46
|
-
filename
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
data/idrac-0.1.7/lib/idrac.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'httparty'
|
4
|
-
require 'nokogiri'
|
5
|
-
require 'faraday'
|
6
|
-
require 'faraday/multipart'
|
7
|
-
require 'base64'
|
8
|
-
require 'uri'
|
9
|
-
# If dev, required debug
|
10
|
-
require 'debug' if ENV['RUBY_ENV'] == 'development'
|
11
|
-
|
12
|
-
require_relative "idrac/version"
|
13
|
-
require_relative "idrac/client"
|
14
|
-
require_relative "idrac/screenshot"
|
15
|
-
require_relative "idrac/firmware"
|
16
|
-
|
17
|
-
module IDRAC
|
18
|
-
class Error < StandardError; end
|
19
|
-
|
20
|
-
def self.new(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true)
|
21
|
-
Client.new(
|
22
|
-
host: host,
|
23
|
-
username: username,
|
24
|
-
password: password,
|
25
|
-
port: port,
|
26
|
-
use_ssl: use_ssl,
|
27
|
-
verify_ssl: verify_ssl
|
28
|
-
)
|
29
|
-
end
|
30
|
-
end
|
data/idrac-0.1.7/sig/idrac.rbs
DELETED