idrac 0.1.26 → 0.1.29
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 +22 -4
- data/bin/idrac +59 -16
- data/lib/idrac/error.rb +3 -0
- data/lib/idrac/firmware.rb +324 -175
- data/lib/idrac/firmware_catalog.rb +314 -0
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac.rb +4 -9
- data/test_firmware_update.rb +70 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c080d98bd3ca44b9cc006ba66cc443ec410dcfb6bc1b712141720cc82998884
|
4
|
+
data.tar.gz: fe0e09d1e3089c5d354abf6f786624d5afd7dfbcd7b2984d84a72be9936dcf9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 479b4ceb21686d4803a3f780f8fe19fcd1741f17cf3b1e24cd0039bd7758688810260dd939fe60cfe07b3b829feefcfbeee4953826f315b65707d2467121fa6c
|
7
|
+
data.tar.gz: 1855a7715e1267c505cc20f9dfd9a9e0665b78ac4db5b06a6984503bb88fb849e7f41b9ebeb68b6e67a70d0b663ea3a6f9b7bbecd1e4c5e194aff198ca9b5238
|
data/README.md
CHANGED
@@ -37,8 +37,10 @@ idrac screenshot --host=192.168.1.100 --username=root --password=calvin
|
|
37
37
|
# Specify a custom output filename
|
38
38
|
idrac screenshot --host=192.168.1.100 --username=root --password=calvin --output=my_screenshot.png
|
39
39
|
|
40
|
-
# Download the Dell firmware catalog
|
41
|
-
idrac
|
40
|
+
# Download the Dell firmware catalog (no host required)
|
41
|
+
idrac catalog download
|
42
|
+
# or
|
43
|
+
idrac firmware:catalog
|
42
44
|
|
43
45
|
# Check firmware status and available updates
|
44
46
|
idrac firmware:status --host=192.168.1.100 --username=root --password=calvin
|
@@ -93,8 +95,9 @@ puts "Screenshot saved to: #{filename}"
|
|
93
95
|
# Firmware operations
|
94
96
|
firmware = IDRAC::Firmware.new(client)
|
95
97
|
|
96
|
-
# Download catalog
|
97
|
-
|
98
|
+
# Download catalog (no client required)
|
99
|
+
catalog = IDRAC::FirmwareCatalog.new
|
100
|
+
catalog_path = catalog.download
|
98
101
|
|
99
102
|
# Get system inventory
|
100
103
|
inventory = firmware.get_system_inventory
|
@@ -127,6 +130,21 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
127
130
|
|
128
131
|
## Changelog
|
129
132
|
|
133
|
+
### Version 0.1.28
|
134
|
+
- **Improved Firmware Update Checking**: Completely redesigned the firmware update checking process
|
135
|
+
- Added a dedicated `FirmwareCatalog` class for better separation of concerns
|
136
|
+
- Improved component matching with more accurate detection of available updates
|
137
|
+
- Enhanced output format with a tabular display showing component details, versions, and update status
|
138
|
+
- Added system model detection for more accurate firmware matching
|
139
|
+
- Improved version comparison logic for different version formats (numeric, Dell A00 format, etc.)
|
140
|
+
- Better handling of network adapters and other component types
|
141
|
+
|
142
|
+
### Version 0.1.27
|
143
|
+
- **Removed Host Requirement for Catalog Download**: The `catalog download` and `firmware:catalog` commands no longer require the `--host` parameter
|
144
|
+
- Added a dedicated `catalog` command that can be used directly: `idrac catalog download`
|
145
|
+
- The catalog download functionality can now be used without an iDRAC connection
|
146
|
+
- Updated the Ruby API to support catalog downloads without a client
|
147
|
+
|
130
148
|
### Version 0.1.26
|
131
149
|
- **Improved Redfish Session Creation**: Fixed issues with the Redfish session creation process
|
132
150
|
- Added multiple fallback methods for creating sessions with different iDRAC versions
|
data/bin/idrac
CHANGED
@@ -13,7 +13,8 @@ require "idrac"
|
|
13
13
|
|
14
14
|
module IDRAC
|
15
15
|
class CLI < Thor
|
16
|
-
|
16
|
+
# Make host not required for all commands
|
17
|
+
class_option :host, type: :string, required: false, desc: "iDRAC host address"
|
17
18
|
class_option :username, type: :string, required: false, default: "root", desc: "iDRAC username (default: root)"
|
18
19
|
class_option :password, type: :string, required: false, default: "calvin", desc: "iDRAC password (default: calvin)"
|
19
20
|
class_option :port, type: :numeric, default: 443, desc: "iDRAC port"
|
@@ -25,6 +26,9 @@ module IDRAC
|
|
25
26
|
method_option :wait, type: :boolean, default: true, desc: "Wait for the update to complete"
|
26
27
|
method_option :timeout, type: :numeric, default: 3600, desc: "Timeout in seconds when waiting"
|
27
28
|
def firmware_update(path)
|
29
|
+
# Ensure host is provided for commands that need it
|
30
|
+
ensure_host_provided
|
31
|
+
|
28
32
|
check_ssl_verification
|
29
33
|
client = create_client
|
30
34
|
firmware = IDRAC::Firmware.new(client)
|
@@ -42,24 +46,25 @@ module IDRAC
|
|
42
46
|
|
43
47
|
desc "firmware:catalog [DIRECTORY]", "Download Dell firmware catalog"
|
44
48
|
def firmware_catalog(directory = nil)
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
# This command doesn't require a host
|
50
|
+
# Create a FirmwareCatalog instance directly
|
51
|
+
catalog = IDRAC::FirmwareCatalog.new
|
48
52
|
|
49
53
|
begin
|
50
|
-
catalog_path =
|
54
|
+
catalog_path = catalog.download(directory)
|
51
55
|
puts "Catalog downloaded to: #{catalog_path}"
|
52
56
|
rescue IDRAC::Error => e
|
53
57
|
puts "Error: #{e.message}"
|
54
58
|
exit 1
|
55
|
-
ensure
|
56
|
-
client.logout
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
60
62
|
desc "firmware:status", "Show current firmware status and available updates"
|
61
63
|
method_option :catalog, type: :string, desc: "Path to existing catalog file"
|
62
64
|
def firmware_status
|
65
|
+
# Ensure host is provided for commands that need it
|
66
|
+
ensure_host_provided
|
67
|
+
|
63
68
|
check_ssl_verification
|
64
69
|
client = create_client
|
65
70
|
firmware = IDRAC::Firmware.new(client)
|
@@ -85,17 +90,12 @@ module IDRAC
|
|
85
90
|
# Check for updates if catalog is available
|
86
91
|
if options[:catalog] || File.exist?(default_catalog)
|
87
92
|
catalog_path = options[:catalog] || default_catalog
|
88
|
-
puts "\nChecking for updates using catalog: #{catalog_path}"
|
89
93
|
|
94
|
+
# Check for updates using the firmware class
|
90
95
|
updates = firmware.check_updates(catalog_path)
|
91
96
|
|
92
97
|
if updates.empty?
|
93
98
|
puts "No updates available."
|
94
|
-
else
|
95
|
-
puts "\nAvailable Updates:"
|
96
|
-
updates.each do |update|
|
97
|
-
puts " #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
|
98
|
-
end
|
99
99
|
end
|
100
100
|
else
|
101
101
|
puts "\nTo check for updates, download the catalog first with 'idrac firmware:catalog'"
|
@@ -111,6 +111,9 @@ module IDRAC
|
|
111
111
|
desc "firmware:interactive", "Interactive firmware update"
|
112
112
|
method_option :catalog, type: :string, desc: "Path to existing catalog file"
|
113
113
|
def firmware_interactive
|
114
|
+
# Ensure host is provided for commands that need it
|
115
|
+
ensure_host_provided
|
116
|
+
|
114
117
|
check_ssl_verification
|
115
118
|
client = create_client
|
116
119
|
firmware = IDRAC::Firmware.new(client)
|
@@ -129,11 +132,40 @@ module IDRAC
|
|
129
132
|
# If still no catalog, download it
|
130
133
|
if catalog_path.nil?
|
131
134
|
puts "No catalog found. Downloading..."
|
132
|
-
|
135
|
+
catalog = IDRAC::FirmwareCatalog.new
|
136
|
+
catalog_path = catalog.download
|
133
137
|
end
|
134
138
|
|
135
|
-
firmware.
|
136
|
-
|
139
|
+
puts "Starting interactive firmware update. Please note:"
|
140
|
+
puts "- The iDRAC can only process one firmware update at a time"
|
141
|
+
puts "- Updates may take several minutes to complete"
|
142
|
+
puts "- For BIOS updates, a server reboot will be required to apply the update"
|
143
|
+
puts "- If you encounter errors, check the iDRAC web interface for active jobs"
|
144
|
+
puts ""
|
145
|
+
|
146
|
+
begin
|
147
|
+
firmware.interactive_update(catalog_path)
|
148
|
+
rescue ArgumentError => e
|
149
|
+
puts "Error: #{e.message}"
|
150
|
+
puts "This could be due to an issue with the interactive update process."
|
151
|
+
puts "If you're seeing a 'job ID not found' error, it might be because:"
|
152
|
+
puts "1. The firmware update job wasn't created properly"
|
153
|
+
puts "2. There's already an update in progress"
|
154
|
+
puts "3. The iDRAC needs time to process the previous request"
|
155
|
+
puts "\nTry again in a few minutes or check the iDRAC web interface for active jobs."
|
156
|
+
rescue IDRAC::Error => e
|
157
|
+
if e.message.include?("already in progress")
|
158
|
+
puts "Error: #{e.message}"
|
159
|
+
puts "\nTroubleshooting steps:"
|
160
|
+
puts "1. Check the iDRAC web interface under Maintenance > System Update for active jobs"
|
161
|
+
puts "2. Wait for any existing updates to complete (can take 15-30 minutes)"
|
162
|
+
puts "3. If no updates appear to be in progress, you may need to restart the iDRAC"
|
163
|
+
puts " (iDRAC web interface > Settings > iDRAC Settings > Reset iDRAC)"
|
164
|
+
else
|
165
|
+
puts "Error: #{e.message}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
rescue => e
|
137
169
|
puts "Error: #{e.message}"
|
138
170
|
exit 1
|
139
171
|
ensure
|
@@ -144,6 +176,9 @@ module IDRAC
|
|
144
176
|
desc "screenshot", "Take a screenshot of the current iDRAC console"
|
145
177
|
method_option :output, type: :string, desc: "Output filename (default: idrac_screenshot_timestamp.png)"
|
146
178
|
def screenshot
|
179
|
+
# Ensure host is provided for commands that need it
|
180
|
+
ensure_host_provided
|
181
|
+
|
147
182
|
check_ssl_verification
|
148
183
|
client = create_client
|
149
184
|
|
@@ -175,6 +210,13 @@ module IDRAC
|
|
175
210
|
|
176
211
|
private
|
177
212
|
|
213
|
+
def ensure_host_provided
|
214
|
+
unless options[:host]
|
215
|
+
puts "Error: No value provided for required option '--host'"
|
216
|
+
exit 1
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
178
220
|
def check_ssl_verification
|
179
221
|
# If verify_ssl is not explicitly set in the command line, show a warning
|
180
222
|
unless ARGV.include?('--verify-ssl') || ARGV.include?('--no-verify-ssl')
|
@@ -198,4 +240,5 @@ module IDRAC
|
|
198
240
|
end
|
199
241
|
end
|
200
242
|
|
243
|
+
# Start the CLI
|
201
244
|
IDRAC::CLI.start(ARGV)
|
data/lib/idrac/error.rb
ADDED
data/lib/idrac/firmware.rb
CHANGED
@@ -5,6 +5,8 @@ require 'json'
|
|
5
5
|
require 'nokogiri'
|
6
6
|
require 'fileutils'
|
7
7
|
require 'securerandom'
|
8
|
+
require 'set'
|
9
|
+
require_relative 'firmware_catalog'
|
8
10
|
|
9
11
|
module IDRAC
|
10
12
|
class Firmware
|
@@ -22,6 +24,9 @@ module IDRAC
|
|
22
24
|
raise Error, "Firmware file not found: #{firmware_path}"
|
23
25
|
end
|
24
26
|
|
27
|
+
# Ensure we have a client
|
28
|
+
raise Error, "Client is required for firmware update" unless client
|
29
|
+
|
25
30
|
# Login to iDRAC
|
26
31
|
client.login unless client.instance_variable_get(:@session_id)
|
27
32
|
|
@@ -37,43 +42,15 @@ module IDRAC
|
|
37
42
|
end
|
38
43
|
|
39
44
|
def download_catalog(output_dir = nil)
|
40
|
-
# Use
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
catalog_gz_path = File.join(output_dir, "Catalog.xml.gz")
|
45
|
-
catalog_path = File.join(output_dir, "Catalog.xml")
|
46
|
-
|
47
|
-
puts "Downloading Dell catalog from #{CATALOG_URL}..."
|
48
|
-
|
49
|
-
uri = URI.parse(CATALOG_URL)
|
50
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
51
|
-
request = Net::HTTP::Get.new(uri)
|
52
|
-
http.request(request) do |response|
|
53
|
-
if response.code == "200"
|
54
|
-
File.open(catalog_gz_path, 'wb') do |file|
|
55
|
-
response.read_body do |chunk|
|
56
|
-
file.write(chunk)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
else
|
60
|
-
raise Error, "Failed to download catalog: #{response.code} #{response.message}"
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
puts "Extracting catalog..."
|
66
|
-
system("gunzip -f #{catalog_gz_path}")
|
67
|
-
|
68
|
-
if File.exist?(catalog_path)
|
69
|
-
puts "Catalog downloaded and extracted to #{catalog_path}"
|
70
|
-
return catalog_path
|
71
|
-
else
|
72
|
-
raise Error, "Failed to extract catalog"
|
73
|
-
end
|
45
|
+
# Use the new FirmwareCatalog class
|
46
|
+
catalog = FirmwareCatalog.new
|
47
|
+
catalog.download(output_dir)
|
74
48
|
end
|
75
49
|
|
76
50
|
def get_system_inventory
|
51
|
+
# Ensure we have a client
|
52
|
+
raise Error, "Client is required for system inventory" unless client
|
53
|
+
|
77
54
|
puts "Retrieving system inventory..."
|
78
55
|
|
79
56
|
# Get basic system information
|
@@ -133,150 +110,172 @@ module IDRAC
|
|
133
110
|
end
|
134
111
|
|
135
112
|
def check_updates(catalog_path = nil)
|
113
|
+
# Ensure we have a client for system inventory
|
114
|
+
raise Error, "Client is required for checking updates" unless client
|
115
|
+
|
136
116
|
# Download catalog if not provided
|
137
117
|
catalog_path ||= download_catalog
|
138
118
|
|
139
119
|
# Get system inventory
|
140
120
|
inventory = get_system_inventory
|
141
121
|
|
142
|
-
#
|
143
|
-
|
122
|
+
# Create a FirmwareCatalog instance
|
123
|
+
catalog = FirmwareCatalog.new(catalog_path)
|
144
124
|
|
145
|
-
# Extract
|
125
|
+
# Extract system information
|
126
|
+
system_model = inventory[:system][:model]
|
146
127
|
service_tag = inventory[:system][:service_tag]
|
147
128
|
|
148
129
|
puts "Checking updates for system with service tag: #{service_tag}"
|
130
|
+
puts "Searching for updates for model: #{system_model}"
|
149
131
|
|
150
|
-
# Find
|
151
|
-
|
132
|
+
# Find system models in the catalog
|
133
|
+
models = catalog.find_system_models(system_model)
|
152
134
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
# Use the ID as the key to avoid duplicates
|
157
|
-
current_versions[fw[:id]] = {
|
158
|
-
name: fw[:name],
|
159
|
-
version: fw[:version],
|
160
|
-
updateable: fw[:updateable],
|
161
|
-
identifiers: extract_identifiers(fw[:name]) # Extract identifiers for better matching
|
162
|
-
}
|
135
|
+
if models.empty?
|
136
|
+
puts "No matching system model found in catalog"
|
137
|
+
return []
|
163
138
|
end
|
164
139
|
|
165
|
-
#
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
140
|
+
# Use the first matching model
|
141
|
+
model = models.first
|
142
|
+
puts "Found system IDs for #{model[:name]}: #{model[:id]}"
|
143
|
+
|
144
|
+
# Find updates for this system
|
145
|
+
catalog_updates = catalog.find_updates_for_system(model[:id])
|
146
|
+
puts "Found #{catalog_updates.size} firmware updates for #{model[:name]}"
|
147
|
+
|
148
|
+
# Compare current firmware with available updates
|
149
|
+
updates = []
|
150
|
+
|
151
|
+
# Print header for firmware comparison table
|
152
|
+
puts "\nFirmware Version Comparison:"
|
153
|
+
puts "=" * 100
|
154
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
|
155
|
+
puts "-" * 100
|
156
|
+
|
157
|
+
# Track components we've already displayed to avoid duplicates
|
158
|
+
displayed_components = Set.new
|
159
|
+
|
160
|
+
# First show current firmware with available updates
|
161
|
+
inventory[:firmware].each do |fw|
|
162
|
+
# Make sure firmware name is not nil
|
163
|
+
firmware_name = fw[:name] || ""
|
173
164
|
|
174
|
-
#
|
175
|
-
|
165
|
+
# Skip if we've already displayed this component
|
166
|
+
next if displayed_components.include?(firmware_name.downcase)
|
167
|
+
displayed_components.add(firmware_name.downcase)
|
176
168
|
|
177
|
-
#
|
178
|
-
|
179
|
-
matched = false
|
169
|
+
# Extract key identifiers from the firmware name
|
170
|
+
identifiers = extract_identifiers(firmware_name)
|
180
171
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
# Normalize names for comparison
|
186
|
-
catalog_name = name.downcase.strip
|
187
|
-
firmware_name = fw_info[:name].downcase.strip
|
188
|
-
|
189
|
-
# Check for matches using multiple strategies
|
190
|
-
match_found = false
|
191
|
-
|
192
|
-
# 1. Check if names contain each other
|
193
|
-
if catalog_name.include?(firmware_name) || firmware_name.include?(catalog_name)
|
194
|
-
match_found = true
|
195
|
-
end
|
172
|
+
# Try to find a matching update
|
173
|
+
matching_updates = catalog_updates.select do |update|
|
174
|
+
update_name = update[:name] || ""
|
196
175
|
|
197
|
-
#
|
198
|
-
|
199
|
-
|
200
|
-
|
176
|
+
# Check if any of our identifiers match the update name
|
177
|
+
identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
|
178
|
+
# Or if the update name contains the firmware name
|
179
|
+
update_name.downcase.include?(firmware_name.downcase) ||
|
180
|
+
# Or if the firmware name contains the update name
|
181
|
+
firmware_name.downcase.include?(update_name.downcase)
|
182
|
+
end
|
183
|
+
|
184
|
+
if matching_updates.any?
|
185
|
+
# Use the first matching update
|
186
|
+
update = matching_updates.first
|
201
187
|
|
202
|
-
#
|
203
|
-
|
204
|
-
# Check if any identifier from firmware matches any identifier from catalog
|
205
|
-
if (fw_info[:identifiers] & catalog_identifiers).any?
|
206
|
-
match_found = true
|
207
|
-
end
|
208
|
-
end
|
188
|
+
# Check if version is newer
|
189
|
+
needs_update = catalog.compare_versions(fw[:version], update[:version])
|
209
190
|
|
210
|
-
#
|
211
|
-
if
|
191
|
+
# Add to updates list if needed
|
192
|
+
if needs_update && fw[:updateable]
|
212
193
|
updates << {
|
213
|
-
name: name,
|
214
|
-
current_version:
|
215
|
-
available_version: version,
|
216
|
-
path: path,
|
217
|
-
component_type: component_type,
|
218
|
-
|
194
|
+
name: fw[:name],
|
195
|
+
current_version: fw[:version],
|
196
|
+
available_version: update[:version],
|
197
|
+
path: update[:path],
|
198
|
+
component_type: update[:component_type],
|
199
|
+
category: update[:category],
|
200
|
+
download_url: update[:download_url]
|
219
201
|
}
|
220
202
|
|
221
|
-
#
|
222
|
-
|
203
|
+
# Print row with update available
|
204
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
205
|
+
fw[:name].to_s[0..29],
|
206
|
+
fw[:version],
|
207
|
+
update[:version],
|
208
|
+
fw[:updateable] ? "Yes" : "No",
|
209
|
+
update[:category] || "N/A",
|
210
|
+
"UPDATE AVAILABLE"
|
211
|
+
]
|
212
|
+
else
|
213
|
+
# Print row with no update needed
|
214
|
+
status = if !needs_update
|
215
|
+
"Current"
|
216
|
+
elsif !fw[:updateable]
|
217
|
+
"Not updateable"
|
218
|
+
else
|
219
|
+
"No update needed"
|
220
|
+
end
|
221
|
+
|
222
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
223
|
+
fw[:name].to_s[0..29],
|
224
|
+
fw[:version],
|
225
|
+
update[:version] || "N/A",
|
226
|
+
fw[:updateable] ? "Yes" : "No",
|
227
|
+
update[:category] || "N/A",
|
228
|
+
status
|
229
|
+
]
|
223
230
|
end
|
231
|
+
else
|
232
|
+
# No matching update found
|
233
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
234
|
+
fw[:name].to_s[0..29],
|
235
|
+
fw[:version],
|
236
|
+
"N/A",
|
237
|
+
fw[:updateable] ? "Yes" : "No",
|
238
|
+
"N/A",
|
239
|
+
"No update available"
|
240
|
+
]
|
224
241
|
end
|
225
242
|
end
|
226
243
|
|
227
|
-
updates
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
end
|
254
|
-
|
255
|
-
if name.include?("BIOS")
|
256
|
-
identifiers << "BIOS"
|
257
|
-
end
|
258
|
-
|
259
|
-
if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
|
260
|
-
identifiers << "iDRAC"
|
261
|
-
end
|
262
|
-
|
263
|
-
if name.include?("Power Supply") || name.include?("PSU")
|
264
|
-
identifiers << "PSU"
|
265
|
-
end
|
266
|
-
|
267
|
-
if name.include?("Lifecycle Controller")
|
268
|
-
identifiers << "LC"
|
244
|
+
# Then show available updates that don't match any current firmware
|
245
|
+
catalog_updates.each do |update|
|
246
|
+
update_name = update[:name] || ""
|
247
|
+
|
248
|
+
# Skip if we've already displayed this component
|
249
|
+
next if displayed_components.include?(update_name.downcase)
|
250
|
+
displayed_components.add(update_name.downcase)
|
251
|
+
|
252
|
+
# Skip if this update was already matched to a current firmware
|
253
|
+
next if inventory[:firmware].any? do |fw|
|
254
|
+
firmware_name = fw[:name] || ""
|
255
|
+
identifiers = extract_identifiers(firmware_name)
|
256
|
+
|
257
|
+
identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
|
258
|
+
update_name.downcase.include?(firmware_name.downcase) ||
|
259
|
+
firmware_name.downcase.include?(update_name.downcase)
|
260
|
+
end
|
261
|
+
|
262
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
263
|
+
update_name.to_s[0..29],
|
264
|
+
"Not Installed",
|
265
|
+
update[:version] || "Unknown",
|
266
|
+
"N/A",
|
267
|
+
update[:category] || "N/A",
|
268
|
+
"NEW COMPONENT"
|
269
|
+
]
|
269
270
|
end
|
270
271
|
|
271
|
-
|
272
|
-
identifiers << "CPLD"
|
273
|
-
end
|
272
|
+
puts "=" * 100
|
274
273
|
|
275
|
-
|
274
|
+
updates
|
276
275
|
end
|
277
276
|
|
278
|
-
def interactive_update(catalog_path = nil)
|
279
|
-
updates = check_updates(catalog_path)
|
277
|
+
def interactive_update(catalog_path = nil, selected_updates = nil)
|
278
|
+
updates = selected_updates || check_updates(catalog_path)
|
280
279
|
|
281
280
|
if updates.empty?
|
282
281
|
puts "No updates available for your system."
|
@@ -288,24 +287,30 @@ module IDRAC
|
|
288
287
|
puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
|
289
288
|
end
|
290
289
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
290
|
+
# If selected_updates is provided, use those directly
|
291
|
+
if selected_updates
|
292
|
+
selected = selected_updates
|
293
|
+
else
|
294
|
+
# Otherwise prompt the user for selection
|
295
|
+
puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):"
|
296
|
+
choice = STDIN.gets.chomp
|
297
|
+
|
298
|
+
return if choice.downcase == 'q'
|
299
|
+
|
300
|
+
selected = if choice.downcase == 'all'
|
301
|
+
updates
|
302
|
+
else
|
303
|
+
index = choice.to_i - 1
|
304
|
+
if index >= 0 && index < updates.size
|
305
|
+
[updates[index]]
|
306
|
+
else
|
307
|
+
puts "Invalid selection."
|
308
|
+
return
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
307
312
|
|
308
|
-
|
313
|
+
selected.each do |update|
|
309
314
|
puts "Downloading #{update[:name]} version #{update[:available_version]}..."
|
310
315
|
|
311
316
|
# Create temp directory
|
@@ -334,8 +339,28 @@ module IDRAC
|
|
334
339
|
end
|
335
340
|
|
336
341
|
puts "Installing #{update[:name]} version #{update[:available_version]}..."
|
337
|
-
|
338
|
-
|
342
|
+
|
343
|
+
begin
|
344
|
+
job_id = update(update_path, wait: true)
|
345
|
+
puts "Update completed with job ID: #{job_id}"
|
346
|
+
rescue IDRAC::Error => e
|
347
|
+
if e.message.include?("already in progress")
|
348
|
+
puts "Error: #{e.message}"
|
349
|
+
puts "\nTips for resolving this issue:"
|
350
|
+
puts "1. Wait for any existing firmware updates to complete (check iDRAC web interface)"
|
351
|
+
puts "2. Restart the iDRAC if no updates appear to be in progress (Settings > iDRAC Settings > Reset iDRAC)"
|
352
|
+
puts "3. Try again after a few minutes"
|
353
|
+
elsif e.message.include?("job ID") || e.message.include?("job not found")
|
354
|
+
puts "Error: #{e.message}"
|
355
|
+
puts "\nThe job ID could not be found or monitored. This could be because:"
|
356
|
+
puts "1. The iDRAC is busy processing other requests"
|
357
|
+
puts "2. The job was created but not properly tracked"
|
358
|
+
puts "3. The iDRAC firmware may need to be updated first"
|
359
|
+
puts "\nCheck the iDRAC web interface to see if the update was actually initiated."
|
360
|
+
else
|
361
|
+
puts "Error during firmware update: #{e.message}"
|
362
|
+
end
|
363
|
+
end
|
339
364
|
|
340
365
|
ensure
|
341
366
|
# Clean up temp directory
|
@@ -383,28 +408,103 @@ module IDRAC
|
|
383
408
|
post_body << "\r\n--#{boundary}--\r\n"
|
384
409
|
|
385
410
|
# Upload the firmware
|
386
|
-
|
411
|
+
begin
|
412
|
+
response = client.authenticated_request(
|
413
|
+
:post,
|
414
|
+
http_push_uri,
|
415
|
+
{
|
416
|
+
headers: {
|
417
|
+
'Content-Type' => "multipart/form-data; boundary=#{boundary}",
|
418
|
+
'If-Match' => etag
|
419
|
+
},
|
420
|
+
body: post_body.join
|
421
|
+
}
|
422
|
+
)
|
423
|
+
rescue => e
|
424
|
+
# Check if the error is about a deployment already in progress
|
425
|
+
if e.message.include?("A deployment or update operation is already in progress")
|
426
|
+
raise Error, "A firmware update is already in progress on the iDRAC. Please wait for it to complete before starting another update. You can check the status in the iDRAC web interface under Maintenance > System Update."
|
427
|
+
else
|
428
|
+
# Re-raise the original error
|
429
|
+
raise
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
if response.status < 200 || response.status >= 300
|
434
|
+
error_message = response.body.to_s
|
435
|
+
|
436
|
+
# Check for specific error messages in the response
|
437
|
+
if error_message.include?("A deployment or update operation is already in progress")
|
438
|
+
raise Error, "A firmware update is already in progress on the iDRAC. Please wait for it to complete before starting another update. You can check the status in the iDRAC web interface under Maintenance > System Update."
|
439
|
+
elsif error_message.include?("SUP0108")
|
440
|
+
raise Error, "iDRAC Error SUP0108: A deployment or update operation is already in progress. Wait for the operation to conclude and then try again."
|
441
|
+
else
|
442
|
+
raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Extract upload ID from response
|
447
|
+
response_data = JSON.parse(response.body)
|
448
|
+
upload_id = response_data['Id'] || response_data['TaskId']
|
449
|
+
|
450
|
+
if upload_id.nil?
|
451
|
+
raise Error, "Failed to extract upload ID from firmware upload response"
|
452
|
+
end
|
453
|
+
|
454
|
+
puts "Firmware file uploaded successfully with ID: #{upload_id}"
|
455
|
+
|
456
|
+
# Step 2: Initiate the firmware update using SimpleUpdate
|
457
|
+
puts "Initiating firmware update using SimpleUpdate..."
|
458
|
+
|
459
|
+
# Construct the image URI using the uploaded file
|
460
|
+
image_uri = "#{http_push_uri}/#{upload_id}"
|
461
|
+
puts "Using ImageURI: #{image_uri}"
|
462
|
+
|
463
|
+
# Call the SimpleUpdate action
|
464
|
+
simple_update_response = client.authenticated_request(
|
387
465
|
:post,
|
388
|
-
|
466
|
+
"/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
|
389
467
|
{
|
390
468
|
headers: {
|
391
|
-
'Content-Type' =>
|
392
|
-
'If-Match' => etag
|
469
|
+
'Content-Type' => 'application/json'
|
393
470
|
},
|
394
|
-
body:
|
471
|
+
body: JSON.generate({
|
472
|
+
'ImageURI' => image_uri,
|
473
|
+
'@Redfish.OperationApplyTime' => 'Immediate'
|
474
|
+
})
|
395
475
|
}
|
396
476
|
)
|
397
477
|
|
398
|
-
if
|
399
|
-
raise Error, "Firmware
|
478
|
+
if simple_update_response.status < 200 || simple_update_response.status >= 300
|
479
|
+
raise Error, "Firmware update initiation failed with status #{simple_update_response.status}: #{simple_update_response.body}"
|
400
480
|
end
|
401
481
|
|
402
482
|
# Extract job ID from response
|
403
|
-
|
404
|
-
|
483
|
+
job_id = nil
|
484
|
+
|
485
|
+
# Try to get job ID from Location header
|
486
|
+
if simple_update_response.headers['location']
|
487
|
+
job_id = simple_update_response.headers['location'].split('/').last
|
488
|
+
end
|
405
489
|
|
490
|
+
# If not found in header, try to get from response body
|
491
|
+
if job_id.nil? && !simple_update_response.body.empty?
|
492
|
+
begin
|
493
|
+
update_data = JSON.parse(simple_update_response.body)
|
494
|
+
if update_data['@odata.id']
|
495
|
+
job_id = update_data['@odata.id'].split('/').last
|
496
|
+
elsif update_data['Id']
|
497
|
+
job_id = update_data['Id']
|
498
|
+
end
|
499
|
+
rescue JSON::ParserError
|
500
|
+
# Not JSON, ignore
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# If still no job ID, use the upload ID as a fallback
|
406
505
|
if job_id.nil?
|
407
|
-
|
506
|
+
job_id = upload_id
|
507
|
+
puts "No job ID found in SimpleUpdate response, using upload ID as job ID"
|
408
508
|
end
|
409
509
|
|
410
510
|
puts "Firmware update job created with ID: #{job_id}"
|
@@ -452,5 +552,54 @@ module IDRAC
|
|
452
552
|
response_data = JSON.parse(response.body)
|
453
553
|
response_data['TaskState'] || 'Unknown'
|
454
554
|
end
|
555
|
+
|
556
|
+
# Helper method to extract identifiers from component names
|
557
|
+
def extract_identifiers(name)
|
558
|
+
return [] unless name
|
559
|
+
|
560
|
+
identifiers = []
|
561
|
+
|
562
|
+
# Extract model numbers like X520, I350, etc.
|
563
|
+
model_matches = name.scan(/[IX]\d{3,4}/)
|
564
|
+
identifiers.concat(model_matches)
|
565
|
+
|
566
|
+
# Extract PERC model like H730
|
567
|
+
perc_matches = name.scan(/[HP]\d{3,4}/)
|
568
|
+
identifiers.concat(perc_matches)
|
569
|
+
|
570
|
+
# Extract other common identifiers
|
571
|
+
if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
|
572
|
+
identifiers << "NIC"
|
573
|
+
end
|
574
|
+
|
575
|
+
if name.include?("PERC") || name.include?("RAID")
|
576
|
+
identifiers << "PERC"
|
577
|
+
# Extract PERC model like H730
|
578
|
+
perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
|
579
|
+
identifiers << perc_match[1] if perc_match
|
580
|
+
end
|
581
|
+
|
582
|
+
if name.include?("BIOS")
|
583
|
+
identifiers << "BIOS"
|
584
|
+
end
|
585
|
+
|
586
|
+
if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
|
587
|
+
identifiers << "iDRAC"
|
588
|
+
end
|
589
|
+
|
590
|
+
if name.include?("Power Supply") || name.include?("PSU")
|
591
|
+
identifiers << "PSU"
|
592
|
+
end
|
593
|
+
|
594
|
+
if name.include?("Lifecycle Controller")
|
595
|
+
identifiers << "LC"
|
596
|
+
end
|
597
|
+
|
598
|
+
if name.include?("CPLD")
|
599
|
+
identifiers << "CPLD"
|
600
|
+
end
|
601
|
+
|
602
|
+
identifiers
|
603
|
+
end
|
455
604
|
end
|
456
605
|
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
module IDRAC
|
7
|
+
class FirmwareCatalog
|
8
|
+
CATALOG_URL = "https://downloads.dell.com/catalog/Catalog.xml.gz"
|
9
|
+
|
10
|
+
attr_reader :catalog_path
|
11
|
+
|
12
|
+
def initialize(catalog_path = nil)
|
13
|
+
@catalog_path = catalog_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def download(output_dir = nil)
|
17
|
+
# Use ~/.idrac as the default directory
|
18
|
+
output_dir ||= File.expand_path("~/.idrac")
|
19
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
20
|
+
|
21
|
+
catalog_gz_path = File.join(output_dir, "Catalog.xml.gz")
|
22
|
+
catalog_path = File.join(output_dir, "Catalog.xml")
|
23
|
+
|
24
|
+
puts "Downloading Dell catalog from #{CATALOG_URL}..."
|
25
|
+
|
26
|
+
uri = URI.parse(CATALOG_URL)
|
27
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
28
|
+
request = Net::HTTP::Get.new(uri)
|
29
|
+
http.request(request) do |response|
|
30
|
+
if response.code == "200"
|
31
|
+
File.open(catalog_gz_path, 'wb') do |file|
|
32
|
+
response.read_body do |chunk|
|
33
|
+
file.write(chunk)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise Error, "Failed to download catalog: #{response.code} #{response.message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "Extracting catalog..."
|
43
|
+
system("gunzip -f #{catalog_gz_path}")
|
44
|
+
|
45
|
+
if File.exist?(catalog_path)
|
46
|
+
puts "Catalog downloaded and extracted to #{catalog_path}"
|
47
|
+
@catalog_path = catalog_path
|
48
|
+
return catalog_path
|
49
|
+
else
|
50
|
+
raise Error, "Failed to extract catalog"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse
|
55
|
+
raise Error, "No catalog path specified" unless @catalog_path
|
56
|
+
raise Error, "Catalog file not found: #{@catalog_path}" unless File.exist?(@catalog_path)
|
57
|
+
|
58
|
+
File.open(@catalog_path) { |f| Nokogiri::XML(f) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_system_models(model_name)
|
62
|
+
doc = parse
|
63
|
+
models = []
|
64
|
+
|
65
|
+
# Extract model code from full model name (e.g., "PowerEdge R640" -> "R640")
|
66
|
+
model_code = nil
|
67
|
+
if model_name.include?("PowerEdge")
|
68
|
+
model_code = model_name.split.last
|
69
|
+
else
|
70
|
+
model_code = model_name
|
71
|
+
end
|
72
|
+
|
73
|
+
puts "Searching for model: #{model_name} (code: #{model_code})"
|
74
|
+
|
75
|
+
# Build a mapping of model names to system IDs
|
76
|
+
model_to_system_id = {}
|
77
|
+
|
78
|
+
doc.xpath('//SupportedSystems/Brand/Model').each do |model|
|
79
|
+
system_id = model['systemID'] || model['id']
|
80
|
+
name = model.at_xpath('Display')&.text
|
81
|
+
code = model.at_xpath('Code')&.text
|
82
|
+
|
83
|
+
if name && system_id
|
84
|
+
model_to_system_id[name] = {
|
85
|
+
name: name,
|
86
|
+
code: code,
|
87
|
+
id: system_id
|
88
|
+
}
|
89
|
+
|
90
|
+
# Also map just the model number (R640, etc.)
|
91
|
+
if name =~ /[RT]\d+/
|
92
|
+
model_short = name.match(/([RT]\d+\w*)/)[1]
|
93
|
+
model_to_system_id[model_short] = {
|
94
|
+
name: name,
|
95
|
+
code: code,
|
96
|
+
id: system_id
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Try exact match first
|
103
|
+
if model_to_system_id[model_name]
|
104
|
+
models << model_to_system_id[model_name]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Try model code match
|
108
|
+
if model_to_system_id[model_code]
|
109
|
+
models << model_to_system_id[model_code]
|
110
|
+
end
|
111
|
+
|
112
|
+
# If we still don't have a match, try a more flexible approach
|
113
|
+
if models.empty?
|
114
|
+
model_to_system_id.each do |name, model_info|
|
115
|
+
if name.include?(model_code) || model_code.include?(name)
|
116
|
+
models << model_info
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# If still no match, try matching by systemID directly
|
122
|
+
if models.empty?
|
123
|
+
doc.xpath('//SupportedSystems/Brand/Model').each do |model|
|
124
|
+
system_id = model['systemID'] || model['id']
|
125
|
+
name = model.at_xpath('Display')&.text
|
126
|
+
code = model.at_xpath('Code')&.text
|
127
|
+
|
128
|
+
if code && code.downcase == model_code.downcase
|
129
|
+
models << {
|
130
|
+
name: name,
|
131
|
+
code: code,
|
132
|
+
id: system_id
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
models.uniq { |m| m[:id] }
|
139
|
+
end
|
140
|
+
|
141
|
+
def find_updates_for_system(system_id)
|
142
|
+
doc = parse
|
143
|
+
updates = []
|
144
|
+
|
145
|
+
# Find all SoftwareComponents
|
146
|
+
doc.xpath("//SoftwareComponent").each do |component|
|
147
|
+
# Check if this component supports our system ID
|
148
|
+
supported_system_ids = component.xpath(".//SupportedSystems/Brand/Model/@systemID | .//SupportedSystems/Brand/Model/@id").map(&:value)
|
149
|
+
|
150
|
+
next unless supported_system_ids.include?(system_id)
|
151
|
+
|
152
|
+
# Get component details
|
153
|
+
name_node = component.xpath("./Name/Display[@lang='en']").first
|
154
|
+
name = name_node ? name_node.text.strip : ""
|
155
|
+
|
156
|
+
component_type_node = component.xpath("./ComponentType/Display[@lang='en']").first
|
157
|
+
component_type = component_type_node ? component_type_node.text.strip : ""
|
158
|
+
|
159
|
+
path = component['path'] || ""
|
160
|
+
category_node = component.xpath("./Category/Display[@lang='en']").first
|
161
|
+
category = category_node ? category_node.text.strip : ""
|
162
|
+
|
163
|
+
version = component['dellVersion'] || component['vendorVersion'] || ""
|
164
|
+
|
165
|
+
# Skip if missing essential information
|
166
|
+
next if name.empty? || path.empty? || version.empty?
|
167
|
+
|
168
|
+
# Only include firmware updates
|
169
|
+
if component_type.include?("Firmware") ||
|
170
|
+
category.include?("BIOS") ||
|
171
|
+
category.include?("Firmware") ||
|
172
|
+
category.include?("iDRAC") ||
|
173
|
+
name.include?("BIOS") ||
|
174
|
+
name.include?("Firmware") ||
|
175
|
+
name.include?("iDRAC")
|
176
|
+
|
177
|
+
updates << {
|
178
|
+
name: name,
|
179
|
+
version: version,
|
180
|
+
path: path,
|
181
|
+
component_type: component_type,
|
182
|
+
category: category,
|
183
|
+
download_url: "https://downloads.dell.com/#{path}"
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
puts "Found #{updates.size} firmware updates for system ID #{system_id}"
|
189
|
+
updates
|
190
|
+
end
|
191
|
+
|
192
|
+
def extract_identifiers(name)
|
193
|
+
return [] unless name
|
194
|
+
|
195
|
+
identifiers = []
|
196
|
+
|
197
|
+
# Extract model numbers like X520, I350, etc.
|
198
|
+
model_matches = name.scan(/[IX]\d{3,4}/)
|
199
|
+
identifiers.concat(model_matches)
|
200
|
+
|
201
|
+
# Extract PERC model like H730
|
202
|
+
perc_matches = name.scan(/[HP]\d{3,4}/)
|
203
|
+
identifiers.concat(perc_matches)
|
204
|
+
|
205
|
+
# Extract other common identifiers
|
206
|
+
if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
|
207
|
+
identifiers << "NIC"
|
208
|
+
end
|
209
|
+
|
210
|
+
if name.include?("PERC") || name.include?("RAID")
|
211
|
+
identifiers << "PERC"
|
212
|
+
# Extract PERC model like H730
|
213
|
+
perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
|
214
|
+
identifiers << perc_match[1] if perc_match
|
215
|
+
end
|
216
|
+
|
217
|
+
if name.include?("BIOS")
|
218
|
+
identifiers << "BIOS"
|
219
|
+
end
|
220
|
+
|
221
|
+
if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
|
222
|
+
identifiers << "iDRAC"
|
223
|
+
end
|
224
|
+
|
225
|
+
if name.include?("Power Supply") || name.include?("PSU")
|
226
|
+
identifiers << "PSU"
|
227
|
+
end
|
228
|
+
|
229
|
+
if name.include?("Lifecycle Controller")
|
230
|
+
identifiers << "LC"
|
231
|
+
end
|
232
|
+
|
233
|
+
if name.include?("CPLD")
|
234
|
+
identifiers << "CPLD"
|
235
|
+
end
|
236
|
+
|
237
|
+
identifiers
|
238
|
+
end
|
239
|
+
|
240
|
+
def match_component(firmware_name, catalog_name)
|
241
|
+
# Normalize names for comparison
|
242
|
+
catalog_name_lower = catalog_name.downcase.strip
|
243
|
+
firmware_name_lower = firmware_name.downcase.strip
|
244
|
+
|
245
|
+
# 1. Direct substring match
|
246
|
+
return true if catalog_name_lower.include?(firmware_name_lower) || firmware_name_lower.include?(catalog_name_lower)
|
247
|
+
|
248
|
+
# 2. Special case for BIOS
|
249
|
+
return true if catalog_name_lower.include?("bios") && firmware_name_lower.include?("bios")
|
250
|
+
|
251
|
+
# 3. Check identifiers
|
252
|
+
firmware_identifiers = extract_identifiers(firmware_name)
|
253
|
+
catalog_identifiers = extract_identifiers(catalog_name)
|
254
|
+
|
255
|
+
return true if (firmware_identifiers & catalog_identifiers).any?
|
256
|
+
|
257
|
+
# 4. Special case for network adapters
|
258
|
+
if (firmware_name_lower.include?("ethernet") || firmware_name_lower.include?("network")) &&
|
259
|
+
(catalog_name_lower.include?("ethernet") || catalog_name_lower.include?("network"))
|
260
|
+
return true
|
261
|
+
end
|
262
|
+
|
263
|
+
# No match found
|
264
|
+
false
|
265
|
+
end
|
266
|
+
|
267
|
+
def compare_versions(current_version, available_version)
|
268
|
+
# If versions are identical, no update needed
|
269
|
+
return false if current_version == available_version
|
270
|
+
|
271
|
+
# If either version is N/A, no update available
|
272
|
+
return false if current_version == "N/A" || available_version == "N/A"
|
273
|
+
|
274
|
+
# Try to handle Dell's version format (e.g., A00, A01, etc.)
|
275
|
+
if available_version.match?(/^[A-Z]\d+$/)
|
276
|
+
# If current version doesn't match Dell's format, assume update is needed
|
277
|
+
return true unless current_version.match?(/^[A-Z]\d+$/)
|
278
|
+
|
279
|
+
# Compare Dell version format (A00 < A01 < A02 < ... < B00 < B01 ...)
|
280
|
+
available_letter = available_version[0]
|
281
|
+
available_number = available_version[1..-1].to_i
|
282
|
+
|
283
|
+
current_letter = current_version[0]
|
284
|
+
current_number = current_version[1..-1].to_i
|
285
|
+
|
286
|
+
return true if current_letter < available_letter
|
287
|
+
return true if current_letter == available_letter && current_number < available_number
|
288
|
+
return false
|
289
|
+
end
|
290
|
+
|
291
|
+
# For numeric versions, try to compare them
|
292
|
+
if current_version.match?(/^[\d\.]+$/) && available_version.match?(/^[\d\.]+$/)
|
293
|
+
current_parts = current_version.split('.').map(&:to_i)
|
294
|
+
available_parts = available_version.split('.').map(&:to_i)
|
295
|
+
|
296
|
+
# Compare each part of the version
|
297
|
+
max_length = [current_parts.length, available_parts.length].max
|
298
|
+
max_length.times do |i|
|
299
|
+
current_part = current_parts[i] || 0
|
300
|
+
available_part = available_parts[i] || 0
|
301
|
+
|
302
|
+
return true if current_part < available_part
|
303
|
+
return false if current_part > available_part
|
304
|
+
end
|
305
|
+
|
306
|
+
# If we get here, versions are equal
|
307
|
+
return false
|
308
|
+
end
|
309
|
+
|
310
|
+
# If we can't determine, assume update is needed
|
311
|
+
true
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
data/lib/idrac/version.rb
CHANGED
data/lib/idrac.rb
CHANGED
@@ -10,21 +10,16 @@ require 'uri'
|
|
10
10
|
require 'debug' if ENV['RUBY_ENV'] == 'development'
|
11
11
|
|
12
12
|
require_relative "idrac/version"
|
13
|
+
require_relative "idrac/error"
|
13
14
|
require_relative "idrac/client"
|
14
15
|
require_relative "idrac/screenshot"
|
15
16
|
require_relative "idrac/firmware"
|
17
|
+
require_relative "idrac/firmware_catalog"
|
16
18
|
|
17
19
|
module IDRAC
|
18
20
|
class Error < StandardError; end
|
19
21
|
|
20
|
-
def self.new(
|
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
|
-
)
|
22
|
+
def self.new(options = {})
|
23
|
+
Client.new(options)
|
29
24
|
end
|
30
25
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'lib/idrac'
|
4
|
+
|
5
|
+
# Create a client
|
6
|
+
client = IDRAC::Client.new(
|
7
|
+
host: '127.0.0.1',
|
8
|
+
username: 'root',
|
9
|
+
password: 'calvin',
|
10
|
+
verify_ssl: false
|
11
|
+
)
|
12
|
+
|
13
|
+
begin
|
14
|
+
# Login to iDRAC
|
15
|
+
client.login
|
16
|
+
puts "Logged in successfully"
|
17
|
+
|
18
|
+
# Create a firmware instance
|
19
|
+
firmware = IDRAC::Firmware.new(client)
|
20
|
+
|
21
|
+
# Get system inventory
|
22
|
+
puts "Getting system inventory..."
|
23
|
+
inventory = firmware.get_system_inventory
|
24
|
+
|
25
|
+
puts "System Information:"
|
26
|
+
puts " Model: #{inventory[:system][:model]}"
|
27
|
+
puts " Manufacturer: #{inventory[:system][:manufacturer]}"
|
28
|
+
puts " Service Tag: #{inventory[:system][:service_tag]}"
|
29
|
+
puts " BIOS Version: #{inventory[:system][:bios_version]}"
|
30
|
+
|
31
|
+
puts "\nInstalled Firmware:"
|
32
|
+
inventory[:firmware].each do |fw|
|
33
|
+
puts " #{fw[:name]}: #{fw[:version]} (#{fw[:updateable] ? 'Updateable' : 'Not Updateable'})"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check for updates
|
37
|
+
catalog_path = File.expand_path("~/.idrac/Catalog.xml")
|
38
|
+
if File.exist?(catalog_path)
|
39
|
+
puts "\nChecking for updates using catalog: #{catalog_path}"
|
40
|
+
updates = firmware.check_updates(catalog_path)
|
41
|
+
|
42
|
+
if updates.empty?
|
43
|
+
puts "No updates available."
|
44
|
+
else
|
45
|
+
puts "\nAvailable Updates:"
|
46
|
+
updates.each_with_index do |update, index|
|
47
|
+
puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# For testing, select the first update
|
51
|
+
if updates.any?
|
52
|
+
selected_update = updates.first
|
53
|
+
puts "\nSelected update: #{selected_update[:name]}: #{selected_update[:current_version]} -> #{selected_update[:available_version]}"
|
54
|
+
|
55
|
+
# Download and install the update
|
56
|
+
puts "Starting interactive update for selected component..."
|
57
|
+
firmware.interactive_update(catalog_path, [selected_update])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
else
|
61
|
+
puts "\nCatalog file not found at #{catalog_path}. Please download the catalog first."
|
62
|
+
end
|
63
|
+
|
64
|
+
rescue IDRAC::Error => e
|
65
|
+
puts "Error: #{e.message}"
|
66
|
+
ensure
|
67
|
+
# Logout
|
68
|
+
client.logout if client.instance_variable_get(:@session_id)
|
69
|
+
puts "Logged out"
|
70
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: idrac
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.29
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
@@ -209,10 +209,13 @@ files:
|
|
209
209
|
- idrac.gemspec
|
210
210
|
- lib/idrac.rb
|
211
211
|
- lib/idrac/client.rb
|
212
|
+
- lib/idrac/error.rb
|
212
213
|
- lib/idrac/firmware.rb
|
214
|
+
- lib/idrac/firmware_catalog.rb
|
213
215
|
- lib/idrac/screenshot.rb
|
214
216
|
- lib/idrac/version.rb
|
215
217
|
- sig/idrac.rbs
|
218
|
+
- test_firmware_update.rb
|
216
219
|
homepage: http://github.com
|
217
220
|
licenses:
|
218
221
|
- MIT
|