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
data/updater.rb
DELETED
@@ -1,729 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'net/http'
|
4
|
-
require 'uri'
|
5
|
-
require 'json'
|
6
|
-
require 'nokogiri'
|
7
|
-
require 'open-uri'
|
8
|
-
require 'fileutils'
|
9
|
-
require 'zip'
|
10
|
-
require 'set'
|
11
|
-
require 'securerandom'
|
12
|
-
|
13
|
-
class DellFirmwareUpdater
|
14
|
-
DEFAULT_USERNAME = 'root'
|
15
|
-
DEFAULT_PASSWORD = 'calvin'
|
16
|
-
DOWNLOAD_DIR = 'dell_firmware_downloads'
|
17
|
-
|
18
|
-
# Dell catalog URLs
|
19
|
-
DELL_CATALOG_BASE = 'https://downloads.dell.com'
|
20
|
-
DELL_CATALOG_URL = "#{DELL_CATALOG_BASE}/catalog/Catalog.xml.gz"
|
21
|
-
DELL_CATALOG_INDEX_URL = "#{DELL_CATALOG_BASE}/catalog/CatalogIndex.xml"
|
22
|
-
|
23
|
-
def initialize(idrac_ip, username = DEFAULT_USERNAME, password = DEFAULT_PASSWORD)
|
24
|
-
@idrac_ip = idrac_ip
|
25
|
-
@username = username
|
26
|
-
@password = password
|
27
|
-
@service_tag = nil
|
28
|
-
@model = nil
|
29
|
-
@redfish_base_url = "https://#{@idrac_ip}/redfish/v1"
|
30
|
-
|
31
|
-
FileUtils.mkdir_p(DOWNLOAD_DIR) unless Dir.exist?(DOWNLOAD_DIR)
|
32
|
-
end
|
33
|
-
|
34
|
-
def get_system_info
|
35
|
-
uri = URI.parse("#{@redfish_base_url}/Systems/System.Embedded.1")
|
36
|
-
|
37
|
-
begin
|
38
|
-
response = make_redfish_request(uri)
|
39
|
-
data = JSON.parse(response.body)
|
40
|
-
@service_tag = data['SKU']
|
41
|
-
@model = data['Model']
|
42
|
-
|
43
|
-
# Try to get additional system identifiers that might match Dell's catalog
|
44
|
-
@system_id = data['SystemType'] || ''
|
45
|
-
@manufacturer = data['Manufacturer'] || ''
|
46
|
-
|
47
|
-
puts "Service Tag: #{@service_tag}"
|
48
|
-
puts "Model: #{@model}"
|
49
|
-
puts "System ID: #{@system_id}" if @system_id
|
50
|
-
puts "Manufacturer: #{@manufacturer}" if @manufacturer
|
51
|
-
|
52
|
-
return {
|
53
|
-
service_tag: @service_tag,
|
54
|
-
model: @model,
|
55
|
-
system_id: @system_id,
|
56
|
-
manufacturer: @manufacturer
|
57
|
-
}
|
58
|
-
rescue => e
|
59
|
-
puts "Error retrieving system information: #{e.message}"
|
60
|
-
nil
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def get_current_firmware_versions
|
65
|
-
uri = URI.parse("#{@redfish_base_url}/UpdateService/FirmwareInventory")
|
66
|
-
|
67
|
-
begin
|
68
|
-
response = make_redfish_request(uri)
|
69
|
-
data = JSON.parse(response.body)
|
70
|
-
|
71
|
-
firmware_list = []
|
72
|
-
|
73
|
-
if data['Members'] && !data['Members'].empty?
|
74
|
-
data['Members'].each do |member|
|
75
|
-
member_uri = member['@odata.id']
|
76
|
-
firmware_uri = URI.parse("https://#{@idrac_ip}#{member_uri}")
|
77
|
-
firmware_response = make_redfish_request(firmware_uri)
|
78
|
-
firmware_data = JSON.parse(firmware_response.body)
|
79
|
-
|
80
|
-
firmware_list << {
|
81
|
-
name: firmware_data['Name'],
|
82
|
-
id: firmware_data['Id'],
|
83
|
-
version: firmware_data['Version'],
|
84
|
-
updateable: firmware_data['Updateable'] || false
|
85
|
-
}
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
firmware_list
|
90
|
-
rescue => e
|
91
|
-
puts "Error retrieving current firmware versions: #{e.message}"
|
92
|
-
[]
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def download_dell_catalog
|
97
|
-
catalog_file = File.join(DOWNLOAD_DIR, 'Catalog.xml.gz')
|
98
|
-
catalog_xml = File.join(DOWNLOAD_DIR, 'Catalog.xml')
|
99
|
-
etag_file = File.join(DOWNLOAD_DIR, 'Catalog.etag')
|
100
|
-
|
101
|
-
# Check if we already have the catalog and if it's current
|
102
|
-
if File.exist?(catalog_xml) && File.exist?(etag_file)
|
103
|
-
current_etag = File.read(etag_file).strip
|
104
|
-
|
105
|
-
# Check if the catalog has changed using HEAD request
|
106
|
-
uri = URI.parse(DELL_CATALOG_URL)
|
107
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
108
|
-
http.use_ssl = true
|
109
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
110
|
-
|
111
|
-
request = Net::HTTP::Head.new(uri.request_uri)
|
112
|
-
response = http.request(request)
|
113
|
-
|
114
|
-
server_etag = response['etag']&.strip
|
115
|
-
|
116
|
-
if server_etag && server_etag == current_etag
|
117
|
-
puts "Catalog is already up to date (ETag: #{current_etag})"
|
118
|
-
return catalog_xml
|
119
|
-
else
|
120
|
-
puts "Catalog has changed. Current ETag: #{current_etag}, Server ETag: #{server_etag}"
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
puts "Downloading Dell catalog..."
|
125
|
-
|
126
|
-
begin
|
127
|
-
# Download the compressed catalog
|
128
|
-
uri = URI.parse(DELL_CATALOG_URL)
|
129
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
130
|
-
http.use_ssl = true
|
131
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
132
|
-
|
133
|
-
request = Net::HTTP::Get.new(uri.request_uri)
|
134
|
-
response = http.request(request)
|
135
|
-
|
136
|
-
# Save the ETag for future checks
|
137
|
-
if response['etag']
|
138
|
-
File.write(etag_file, response['etag'].strip)
|
139
|
-
end
|
140
|
-
|
141
|
-
# Save the catalog
|
142
|
-
File.open(catalog_file, 'wb') do |file|
|
143
|
-
file.write(response.body)
|
144
|
-
end
|
145
|
-
|
146
|
-
# Decompress the catalog
|
147
|
-
system("gunzip -f #{catalog_file}")
|
148
|
-
|
149
|
-
puts "Catalog downloaded and extracted to #{catalog_xml}"
|
150
|
-
return catalog_xml
|
151
|
-
rescue => e
|
152
|
-
puts "Error downloading Dell catalog: #{e.message}"
|
153
|
-
# If download fails but we have an existing catalog, use that
|
154
|
-
if File.exist?(catalog_xml)
|
155
|
-
puts "Using existing catalog file"
|
156
|
-
return catalog_xml
|
157
|
-
end
|
158
|
-
return nil
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def get_available_updates_from_catalog(catalog_path)
|
163
|
-
return [] unless @model && File.exist?(catalog_path)
|
164
|
-
|
165
|
-
begin
|
166
|
-
doc = Nokogiri::XML(File.open(catalog_path))
|
167
|
-
|
168
|
-
# Extract model code from full model name (e.g., "PowerEdge R730" -> "R730")
|
169
|
-
model_code = @model.split(/\s+/).last
|
170
|
-
|
171
|
-
puts "Searching for updates for model: #{@model} (code: #{model_code})"
|
172
|
-
|
173
|
-
# First, build a mapping of model names to system IDs
|
174
|
-
model_to_system_id = {}
|
175
|
-
|
176
|
-
doc.xpath("//SupportedSystems/Brand/Model").each do |model_node|
|
177
|
-
system_id = model_node['systemID']
|
178
|
-
display_node = model_node.xpath("./Display[@lang='en']").first
|
179
|
-
|
180
|
-
if display_node && system_id
|
181
|
-
model_name = display_node.text.strip
|
182
|
-
model_to_system_id[model_name] = system_id
|
183
|
-
|
184
|
-
# Also map just the model number (R730, etc.)
|
185
|
-
if model_name =~ /[RT]\d+/
|
186
|
-
model_short = model_name.match(/([RT]\d+\w*)/)[1]
|
187
|
-
model_to_system_id[model_short] = system_id
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
# Find the system ID for our model
|
193
|
-
target_system_ids = []
|
194
|
-
|
195
|
-
# Try exact match first
|
196
|
-
if model_to_system_id[@model]
|
197
|
-
target_system_ids << model_to_system_id[@model]
|
198
|
-
end
|
199
|
-
|
200
|
-
# Try model code match
|
201
|
-
if model_to_system_id[model_code]
|
202
|
-
target_system_ids << model_to_system_id[model_code]
|
203
|
-
end
|
204
|
-
|
205
|
-
# If we still don't have a match, try a more flexible approach
|
206
|
-
if target_system_ids.empty?
|
207
|
-
model_to_system_id.each do |name, id|
|
208
|
-
if name.include?(model_code) || model_code.include?(name)
|
209
|
-
target_system_ids << id
|
210
|
-
end
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
# Print what we found
|
215
|
-
if target_system_ids.empty?
|
216
|
-
puts "Could not find system ID for #{@model} or #{model_code}"
|
217
|
-
puts "Available models in catalog: #{model_to_system_id.keys.take(10).join(', ')}..."
|
218
|
-
else
|
219
|
-
puts "Found system IDs for #{@model}: #{target_system_ids.join(', ')}"
|
220
|
-
end
|
221
|
-
|
222
|
-
# Find all components for this model
|
223
|
-
updates = []
|
224
|
-
|
225
|
-
# Find SoftwareComponents for this model
|
226
|
-
doc.xpath("//SoftwareComponent").each do |component|
|
227
|
-
# Check if this component supports any of our target system IDs
|
228
|
-
supported_system_ids = component.xpath(".//SupportedSystems/Brand/Model/@systemID").map(&:value)
|
229
|
-
|
230
|
-
match_found = false
|
231
|
-
matching_id = nil
|
232
|
-
|
233
|
-
# Check if any of our target system IDs are supported
|
234
|
-
target_system_ids.each do |target_id|
|
235
|
-
if supported_system_ids.include?(target_id)
|
236
|
-
match_found = true
|
237
|
-
matching_id = target_id
|
238
|
-
break
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# If no direct match, try to match by model name in CDATA
|
243
|
-
if !match_found
|
244
|
-
cdata_content = component.xpath(".//SupportedSystems/Brand/Model/Display").map(&:text).join(" ")
|
245
|
-
if cdata_content.include?(model_code)
|
246
|
-
match_found = true
|
247
|
-
matching_id = "CDATA_match"
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
next unless match_found
|
252
|
-
|
253
|
-
# Get component details
|
254
|
-
name_node = component.xpath("./Name/Display[@lang='en']").first
|
255
|
-
name = name_node ? name_node.text.strip : ""
|
256
|
-
|
257
|
-
component_type_node = component.xpath("./ComponentType/Display[@lang='en']").first
|
258
|
-
component_type = component_type_node ? component_type_node.text.strip : ""
|
259
|
-
|
260
|
-
path = component['path'] || ""
|
261
|
-
category_node = component.xpath("./Category/Display[@lang='en']").first
|
262
|
-
category = category_node ? category_node.text.strip : ""
|
263
|
-
|
264
|
-
release_date = component['releaseDate'] || ""
|
265
|
-
version = component['dellVersion'] || component['vendorVersion'] || ""
|
266
|
-
|
267
|
-
# Only include firmware updates
|
268
|
-
if component_type.include?("Firmware") ||
|
269
|
-
category.include?("BIOS") ||
|
270
|
-
category.include?("Firmware") ||
|
271
|
-
category.include?("iDRAC") ||
|
272
|
-
name.include?("BIOS") ||
|
273
|
-
name.include?("Firmware") ||
|
274
|
-
name.include?("iDRAC")
|
275
|
-
|
276
|
-
download_url = "#{DELL_CATALOG_BASE}/#{path}"
|
277
|
-
|
278
|
-
updates << {
|
279
|
-
name: name,
|
280
|
-
version: version,
|
281
|
-
category: category,
|
282
|
-
release_date: release_date,
|
283
|
-
download_url: download_url,
|
284
|
-
file_name: File.basename(path),
|
285
|
-
matching_system_id: matching_id
|
286
|
-
}
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
puts "Found #{updates.length} firmware updates for #{@model}"
|
291
|
-
updates
|
292
|
-
rescue => e
|
293
|
-
puts "Error parsing Dell catalog: #{e.message}"
|
294
|
-
puts e.backtrace.join("\n")
|
295
|
-
[]
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
def download_firmware(update)
|
300
|
-
return false unless update && update[:download_url]
|
301
|
-
|
302
|
-
begin
|
303
|
-
file_path = File.join(DOWNLOAD_DIR, update[:file_name])
|
304
|
-
|
305
|
-
puts "Downloading #{update[:name]} v#{update[:version]} (#{update[:file_name]})..."
|
306
|
-
File.open(file_path, 'wb') do |file|
|
307
|
-
file.write(URI.open(update[:download_url]).read)
|
308
|
-
end
|
309
|
-
|
310
|
-
puts "Downloaded to #{file_path}"
|
311
|
-
|
312
|
-
# If it's a zip file, extract it to find the DUP or EFI file
|
313
|
-
if file_path.end_with?('.zip')
|
314
|
-
extract_dir = File.join(DOWNLOAD_DIR, File.basename(file_path, '.zip'))
|
315
|
-
FileUtils.mkdir_p(extract_dir)
|
316
|
-
|
317
|
-
Zip::File.open(file_path) do |zip_file|
|
318
|
-
zip_file.each do |entry|
|
319
|
-
# Look for .EFI files which are suitable for Redfish update
|
320
|
-
if entry.name.end_with?('.EFI', '.efi')
|
321
|
-
entry_path = File.join(extract_dir, entry.name)
|
322
|
-
entry.extract(entry_path)
|
323
|
-
puts "Extracted #{entry.name} to #{entry_path}"
|
324
|
-
return entry_path
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
return file_path
|
331
|
-
rescue => e
|
332
|
-
puts "Error downloading firmware: #{e.message}"
|
333
|
-
false
|
334
|
-
end
|
335
|
-
end
|
336
|
-
|
337
|
-
def upload_firmware(file_path)
|
338
|
-
begin
|
339
|
-
# Step 1: Get the HttpPushUri from UpdateService
|
340
|
-
puts "Getting HttpPushUri from UpdateService..."
|
341
|
-
update_service_uri = URI.parse("#{@redfish_base_url}/UpdateService")
|
342
|
-
update_service_response = make_redfish_request(update_service_uri)
|
343
|
-
update_service_data = JSON.parse(update_service_response.body)
|
344
|
-
|
345
|
-
http_push_uri = update_service_data['HttpPushUri']
|
346
|
-
if http_push_uri.nil?
|
347
|
-
http_push_uri = "/redfish/v1/UpdateService/FirmwareInventory"
|
348
|
-
puts "HttpPushUri not found, using default: #{http_push_uri}"
|
349
|
-
else
|
350
|
-
puts "Found HttpPushUri: #{http_push_uri}"
|
351
|
-
end
|
352
|
-
|
353
|
-
# Step 2: Get the ETag for the HttpPushUri
|
354
|
-
uri = URI.parse("https://#{@idrac_ip}#{http_push_uri}")
|
355
|
-
request = Net::HTTP::Get.new(uri)
|
356
|
-
request.basic_auth(@username, @password)
|
357
|
-
|
358
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
359
|
-
http.use_ssl = true
|
360
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
361
|
-
|
362
|
-
response = http.request(request)
|
363
|
-
etag = response['ETag']
|
364
|
-
|
365
|
-
puts "Got ETag: #{etag}"
|
366
|
-
|
367
|
-
# Step 3: Upload the firmware file using multipart/form-data
|
368
|
-
puts "Uploading firmware file to iDRAC..."
|
369
|
-
|
370
|
-
# Create a boundary for multipart/form-data
|
371
|
-
boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
|
372
|
-
|
373
|
-
request = Net::HTTP::Post.new(uri)
|
374
|
-
request.basic_auth(@username, @password)
|
375
|
-
request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
376
|
-
request['If-Match'] = etag
|
377
|
-
|
378
|
-
# Read the file content
|
379
|
-
file_content = File.binread(file_path)
|
380
|
-
filename = File.basename(file_path)
|
381
|
-
|
382
|
-
# Create the multipart body
|
383
|
-
post_body = []
|
384
|
-
post_body << "--#{boundary}\r\n"
|
385
|
-
post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
|
386
|
-
post_body << "Content-Type: application/octet-stream\r\n\r\n"
|
387
|
-
post_body << file_content
|
388
|
-
post_body << "\r\n--#{boundary}--\r\n"
|
389
|
-
|
390
|
-
request.body = post_body.join
|
391
|
-
|
392
|
-
response = http.request(request)
|
393
|
-
|
394
|
-
if response.code.to_i == 201
|
395
|
-
puts "Firmware file uploaded successfully"
|
396
|
-
upload_data = JSON.parse(response.body)
|
397
|
-
available_entry = upload_data['Id']
|
398
|
-
puts "Created entry ID: #{available_entry}"
|
399
|
-
|
400
|
-
# Step 4: Initiate the firmware update using SimpleUpdate
|
401
|
-
puts "Initiating firmware update..."
|
402
|
-
uri = URI.parse("#{@redfish_base_url}/UpdateService/Actions/UpdateService.SimpleUpdate")
|
403
|
-
|
404
|
-
request = Net::HTTP::Post.new(uri)
|
405
|
-
request.basic_auth(@username, @password)
|
406
|
-
request.content_type = 'application/json'
|
407
|
-
|
408
|
-
# Use the URI of the uploaded file
|
409
|
-
image_uri = "#{http_push_uri}/#{available_entry}"
|
410
|
-
puts "Using ImageURI: #{image_uri}"
|
411
|
-
|
412
|
-
request.body = JSON.generate({
|
413
|
-
'ImageURI' => image_uri,
|
414
|
-
'@Redfish.OperationApplyTime' => 'Immediate'
|
415
|
-
})
|
416
|
-
|
417
|
-
response = http.request(request)
|
418
|
-
|
419
|
-
if response.code.to_i >= 200 && response.code.to_i < 300
|
420
|
-
puts "Firmware update initiated successfully"
|
421
|
-
|
422
|
-
# Extract job ID from response
|
423
|
-
job_id = nil
|
424
|
-
if response['Location']
|
425
|
-
job_id = response['Location'].split('/').last
|
426
|
-
elsif response.body && !response.body.empty?
|
427
|
-
begin
|
428
|
-
response_data = JSON.parse(response.body)
|
429
|
-
if response_data['@odata.id']
|
430
|
-
job_id = response_data['@odata.id'].split('/').last
|
431
|
-
elsif response_data['Id']
|
432
|
-
job_id = response_data['Id']
|
433
|
-
end
|
434
|
-
rescue JSON::ParserError
|
435
|
-
# Not JSON, ignore
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
# If we have a job ID, monitor the job
|
440
|
-
if job_id
|
441
|
-
puts "Monitoring job #{job_id}..."
|
442
|
-
monitor_update_job(job_id)
|
443
|
-
else
|
444
|
-
puts "No job ID found. Cannot monitor progress."
|
445
|
-
end
|
446
|
-
|
447
|
-
return true
|
448
|
-
else
|
449
|
-
puts "Firmware update failed: #{response.code} - #{response.body}"
|
450
|
-
return false
|
451
|
-
end
|
452
|
-
else
|
453
|
-
puts "Firmware file upload failed: #{response.code} - #{response.body}"
|
454
|
-
return false
|
455
|
-
end
|
456
|
-
rescue => e
|
457
|
-
puts "Error uploading firmware: #{e.message}"
|
458
|
-
puts e.backtrace.join("\n")
|
459
|
-
false
|
460
|
-
end
|
461
|
-
end
|
462
|
-
|
463
|
-
def monitor_update_job(job_id)
|
464
|
-
uri = URI.parse("#{@redfish_base_url}/TaskService/Tasks/#{job_id}")
|
465
|
-
|
466
|
-
puts "Update job started. This may take several minutes."
|
467
|
-
puts "Progress: [" + " " * 50 + "] 0%"
|
468
|
-
|
469
|
-
completed = false
|
470
|
-
last_message = ""
|
471
|
-
start_time = Time.now
|
472
|
-
|
473
|
-
while !completed && (Time.now - start_time) < 3600 # 1 hour timeout
|
474
|
-
begin
|
475
|
-
response = make_redfish_request(uri)
|
476
|
-
|
477
|
-
if response.code.to_i >= 200 && response.code.to_i < 300
|
478
|
-
job_data = JSON.parse(response.body)
|
479
|
-
|
480
|
-
state = job_data['TaskState'] || ""
|
481
|
-
percent = job_data['PercentComplete'] || 0
|
482
|
-
status = job_data['TaskStatus'] || ""
|
483
|
-
message = job_data['Messages'].first['Message'] rescue ""
|
484
|
-
|
485
|
-
# Only update if something changed
|
486
|
-
if last_message != "#{state} - #{percent}% - #{status} - #{message}"
|
487
|
-
# Clear the current line
|
488
|
-
print "\r" + " " * 80 + "\r"
|
489
|
-
|
490
|
-
# Print progress bar
|
491
|
-
progress = (percent / 2).to_i
|
492
|
-
progress_bar = "[" + "#" * progress + " " * (50 - progress) + "] #{percent}%"
|
493
|
-
puts "Status: #{state} - #{status}"
|
494
|
-
puts "Progress: #{progress_bar}"
|
495
|
-
puts "Message: #{message}" if !message.empty?
|
496
|
-
|
497
|
-
last_message = "#{state} - #{percent}% - #{status} - #{message}"
|
498
|
-
end
|
499
|
-
|
500
|
-
# Check if job is completed
|
501
|
-
if ['Completed', 'Exception', 'Killed', 'Cancelled'].include?(state)
|
502
|
-
completed = true
|
503
|
-
|
504
|
-
if state == 'Completed' && status == 'OK'
|
505
|
-
puts "\nFirmware update completed successfully!"
|
506
|
-
else
|
507
|
-
puts "\nFirmware update failed: #{state} - #{status}"
|
508
|
-
puts "Error message: #{message}" if !message.empty?
|
509
|
-
|
510
|
-
# Try to get more detailed error information
|
511
|
-
if job_data['Messages'] && !job_data['Messages'].empty?
|
512
|
-
job_data['Messages'].each do |msg|
|
513
|
-
puts "- #{msg['Message']}" if msg['Message']
|
514
|
-
end
|
515
|
-
end
|
516
|
-
end
|
517
|
-
end
|
518
|
-
else
|
519
|
-
puts "Error checking job status: #{response.code}"
|
520
|
-
end
|
521
|
-
rescue => e
|
522
|
-
puts "Error monitoring job: #{e.message}"
|
523
|
-
end
|
524
|
-
|
525
|
-
# Don't hammer the API
|
526
|
-
sleep 10 unless completed
|
527
|
-
end
|
528
|
-
|
529
|
-
if !completed
|
530
|
-
puts "Timeout waiting for job to complete. Please check the iDRAC web interface."
|
531
|
-
end
|
532
|
-
end
|
533
|
-
|
534
|
-
def compare_firmware_versions(current_firmware, available_updates)
|
535
|
-
puts "\nFirmware Version Comparison:"
|
536
|
-
puts "=" * 100
|
537
|
-
puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
|
538
|
-
puts "-" * 100
|
539
|
-
|
540
|
-
# Track components we've already displayed to avoid duplicates
|
541
|
-
displayed_components = Set.new
|
542
|
-
|
543
|
-
# First show current firmware with available updates
|
544
|
-
current_firmware.each do |firmware|
|
545
|
-
# Make sure firmware name is not nil
|
546
|
-
firmware_name = firmware[:name] || ""
|
547
|
-
|
548
|
-
# Skip if we've already displayed this component
|
549
|
-
next if displayed_components.include?(firmware_name.downcase)
|
550
|
-
displayed_components.add(firmware_name.downcase)
|
551
|
-
|
552
|
-
# Extract key identifiers from the firmware name
|
553
|
-
# This helps match components like "Intel(R) Ethernet 10G 4P X520" to "Intel NIC Family for X520"
|
554
|
-
identifiers = extract_identifiers(firmware_name)
|
555
|
-
|
556
|
-
# Try to find a matching update
|
557
|
-
matching_update = available_updates.find do |update|
|
558
|
-
update_name = update[:name] || ""
|
559
|
-
|
560
|
-
# Check if any of our identifiers match the update name
|
561
|
-
identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
|
562
|
-
# Or if the update name contains the firmware name
|
563
|
-
update_name.downcase.include?(firmware_name.downcase) ||
|
564
|
-
# Or if the firmware name contains the update name
|
565
|
-
firmware_name.downcase.include?(update_name.downcase)
|
566
|
-
end
|
567
|
-
|
568
|
-
current_version = firmware[:version] || "Unknown"
|
569
|
-
available_version = matching_update ? matching_update[:version] : "N/A"
|
570
|
-
category = matching_update ? matching_update[:category] : "N/A"
|
571
|
-
|
572
|
-
# Determine update status
|
573
|
-
status = if available_version == "N/A"
|
574
|
-
"No update available"
|
575
|
-
elsif available_version == current_version
|
576
|
-
"Current"
|
577
|
-
else
|
578
|
-
"UPDATE AVAILABLE"
|
579
|
-
end
|
580
|
-
|
581
|
-
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
582
|
-
firmware_name.to_s[0..29],
|
583
|
-
current_version,
|
584
|
-
available_version,
|
585
|
-
firmware[:updateable] ? "Yes" : "No",
|
586
|
-
category,
|
587
|
-
status
|
588
|
-
]
|
589
|
-
end
|
590
|
-
|
591
|
-
# Then show available updates that don't match any current firmware
|
592
|
-
available_updates.each do |update|
|
593
|
-
update_name = update[:name] || ""
|
594
|
-
|
595
|
-
# Skip if we've already displayed this component
|
596
|
-
next if displayed_components.include?(update_name.downcase)
|
597
|
-
displayed_components.add(update_name.downcase)
|
598
|
-
|
599
|
-
# Skip if this update was already matched to a current firmware
|
600
|
-
next if current_firmware.any? do |firmware|
|
601
|
-
firmware_name = firmware[:name] || ""
|
602
|
-
identifiers = extract_identifiers(firmware_name)
|
603
|
-
|
604
|
-
identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
|
605
|
-
update_name.downcase.include?(firmware_name.downcase) ||
|
606
|
-
firmware_name.downcase.include?(update_name.downcase)
|
607
|
-
end
|
608
|
-
|
609
|
-
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
610
|
-
update_name.to_s[0..29],
|
611
|
-
"Not Installed",
|
612
|
-
update[:version] || "Unknown",
|
613
|
-
"N/A",
|
614
|
-
update[:category] || "N/A",
|
615
|
-
"NEW COMPONENT"
|
616
|
-
]
|
617
|
-
end
|
618
|
-
|
619
|
-
puts "=" * 100
|
620
|
-
end
|
621
|
-
|
622
|
-
# Helper method to extract identifiers from component names
|
623
|
-
def extract_identifiers(name)
|
624
|
-
return [] unless name
|
625
|
-
|
626
|
-
identifiers = []
|
627
|
-
|
628
|
-
# Extract model numbers like X520, I350, etc.
|
629
|
-
model_matches = name.scan(/[IX]\d{3,4}/)
|
630
|
-
identifiers.concat(model_matches)
|
631
|
-
|
632
|
-
# Extract other common identifiers
|
633
|
-
if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
|
634
|
-
identifiers << "NIC"
|
635
|
-
end
|
636
|
-
|
637
|
-
if name.include?("PERC") || name.include?("RAID")
|
638
|
-
identifiers << "PERC"
|
639
|
-
# Extract PERC model like H730
|
640
|
-
perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
|
641
|
-
identifiers << perc_match[1] if perc_match
|
642
|
-
end
|
643
|
-
|
644
|
-
if name.include?("BIOS")
|
645
|
-
identifiers << "BIOS"
|
646
|
-
end
|
647
|
-
|
648
|
-
if name.include?("iDRAC") || name.include?("IDRAC")
|
649
|
-
identifiers << "iDRAC"
|
650
|
-
end
|
651
|
-
|
652
|
-
if name.include?("Power Supply") || name.include?("PSU")
|
653
|
-
identifiers << "PSU"
|
654
|
-
end
|
655
|
-
|
656
|
-
identifiers
|
657
|
-
end
|
658
|
-
|
659
|
-
private
|
660
|
-
|
661
|
-
def make_redfish_request(uri)
|
662
|
-
request = Net::HTTP::Get.new(uri)
|
663
|
-
request.basic_auth(@username, @password)
|
664
|
-
|
665
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
666
|
-
http.use_ssl = true
|
667
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
668
|
-
|
669
|
-
http.request(request)
|
670
|
-
end
|
671
|
-
end
|
672
|
-
|
673
|
-
# Example usage
|
674
|
-
if __FILE__ == $0
|
675
|
-
if ARGV.length < 1
|
676
|
-
puts "Usage: #{$0} <idrac_ip> [username] [password]"
|
677
|
-
exit 1
|
678
|
-
end
|
679
|
-
|
680
|
-
idrac_ip = ARGV[0]
|
681
|
-
username = ARGV[1] || DellFirmwareUpdater::DEFAULT_USERNAME
|
682
|
-
password = ARGV[2] || DellFirmwareUpdater::DEFAULT_PASSWORD
|
683
|
-
|
684
|
-
updater = DellFirmwareUpdater.new(idrac_ip, username, password)
|
685
|
-
|
686
|
-
# Get system information
|
687
|
-
system_info = updater.get_system_info
|
688
|
-
if system_info
|
689
|
-
# Get current firmware versions
|
690
|
-
current_firmware = updater.get_current_firmware_versions
|
691
|
-
|
692
|
-
# Download Dell catalog
|
693
|
-
catalog_path = updater.download_dell_catalog
|
694
|
-
|
695
|
-
if catalog_path
|
696
|
-
# Get available updates from catalog
|
697
|
-
available_updates = updater.get_available_updates_from_catalog(catalog_path)
|
698
|
-
|
699
|
-
if available_updates.any?
|
700
|
-
# Compare current vs available versions
|
701
|
-
updater.compare_firmware_versions(current_firmware, available_updates)
|
702
|
-
|
703
|
-
# Prompt for which update to download
|
704
|
-
puts "\nAvailable updates:"
|
705
|
-
available_updates.each_with_index do |update, index|
|
706
|
-
puts "#{index + 1}. #{update[:name]} - #{update[:version]} (#{update[:category]})"
|
707
|
-
end
|
708
|
-
|
709
|
-
print "\nEnter the number of the update to download (or 0 to exit): "
|
710
|
-
selection = STDIN.gets.chomp.to_i
|
711
|
-
|
712
|
-
if selection > 0 && selection <= available_updates.length
|
713
|
-
update = available_updates[selection - 1]
|
714
|
-
firmware_path = updater.download_firmware(update)
|
715
|
-
|
716
|
-
if firmware_path
|
717
|
-
print "Do you want to upload and install this firmware? (y/n): "
|
718
|
-
if STDIN.gets.chomp.downcase == 'y'
|
719
|
-
updater.upload_firmware(firmware_path)
|
720
|
-
end
|
721
|
-
end
|
722
|
-
end
|
723
|
-
else
|
724
|
-
puts "No updates found for #{system_info[:model]}"
|
725
|
-
end
|
726
|
-
end
|
727
|
-
end
|
728
|
-
end
|
729
|
-
|