idrac 0.1.26 → 0.1.28
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 +38 -22
- data/lib/idrac/error.rb +3 -0
- data/lib/idrac/firmware.rb +89 -149
- data/lib/idrac/firmware_catalog.rb +243 -0
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac.rb +4 -9
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 180bec6c43d2aced138c9479bbbfe23cec04c6149d2cd66343b5a738c0ce479f
|
4
|
+
data.tar.gz: 10fae4fc88a7d1ba456e3f7dc7d2efbfb8c7500b7839e7b59a2e27d5dae53c7c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0990d968c95f92c2b410b8338c88e8bff464089cc828f9bd49c2fa8bb8111aff7f059fc68d8ff958ccf9c2036af6aebcd2614b96b4ef757bbf93d59f47eecba5'
|
7
|
+
data.tar.gz: bfea9fe661ac565bc5ee652204a927a4c19977abef08bdd51ddf0c79027cc9f40c24d3b21522a9247d8933c22499f99b2bea73be02e0e647f81b42bf7ef2ec53
|
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
@@ -12,6 +12,23 @@ require "thor"
|
|
12
12
|
require "idrac"
|
13
13
|
|
14
14
|
module IDRAC
|
15
|
+
# Standalone catalog command that doesn't require a host
|
16
|
+
class CatalogCommand < Thor
|
17
|
+
desc "download [DIRECTORY]", "Download Dell firmware catalog"
|
18
|
+
def download(directory = nil)
|
19
|
+
# Create a FirmwareCatalog instance
|
20
|
+
catalog = IDRAC::FirmwareCatalog.new
|
21
|
+
|
22
|
+
begin
|
23
|
+
catalog_path = catalog.download(directory)
|
24
|
+
puts "Catalog downloaded to: #{catalog_path}"
|
25
|
+
rescue IDRAC::Error => e
|
26
|
+
puts "Error: #{e.message}"
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
15
32
|
class CLI < Thor
|
16
33
|
class_option :host, type: :string, required: true, desc: "iDRAC host address"
|
17
34
|
class_option :username, type: :string, required: false, default: "root", desc: "iDRAC username (default: root)"
|
@@ -42,19 +59,8 @@ module IDRAC
|
|
42
59
|
|
43
60
|
desc "firmware:catalog [DIRECTORY]", "Download Dell firmware catalog"
|
44
61
|
def firmware_catalog(directory = nil)
|
45
|
-
|
46
|
-
|
47
|
-
firmware = IDRAC::Firmware.new(client)
|
48
|
-
|
49
|
-
begin
|
50
|
-
catalog_path = firmware.download_catalog(directory)
|
51
|
-
puts "Catalog downloaded to: #{catalog_path}"
|
52
|
-
rescue IDRAC::Error => e
|
53
|
-
puts "Error: #{e.message}"
|
54
|
-
exit 1
|
55
|
-
ensure
|
56
|
-
client.logout
|
57
|
-
end
|
62
|
+
# Forward to the catalog command
|
63
|
+
CatalogCommand.new.download(directory)
|
58
64
|
end
|
59
65
|
|
60
66
|
desc "firmware:status", "Show current firmware status and available updates"
|
@@ -85,20 +91,15 @@ module IDRAC
|
|
85
91
|
# Check for updates if catalog is available
|
86
92
|
if options[:catalog] || File.exist?(default_catalog)
|
87
93
|
catalog_path = options[:catalog] || default_catalog
|
88
|
-
puts "\nChecking for updates using catalog: #{catalog_path}"
|
89
94
|
|
95
|
+
# Check for updates using the firmware class
|
90
96
|
updates = firmware.check_updates(catalog_path)
|
91
97
|
|
92
98
|
if updates.empty?
|
93
99
|
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
100
|
end
|
100
101
|
else
|
101
|
-
puts "\nTo check for updates, download the catalog first with 'idrac
|
102
|
+
puts "\nTo check for updates, download the catalog first with 'idrac catalog download'"
|
102
103
|
end
|
103
104
|
rescue IDRAC::Error => e
|
104
105
|
puts "Error: #{e.message}"
|
@@ -129,7 +130,8 @@ module IDRAC
|
|
129
130
|
# If still no catalog, download it
|
130
131
|
if catalog_path.nil?
|
131
132
|
puts "No catalog found. Downloading..."
|
132
|
-
|
133
|
+
catalog = IDRAC::FirmwareCatalog.new
|
134
|
+
catalog_path = catalog.download
|
133
135
|
end
|
134
136
|
|
135
137
|
firmware.interactive_update(catalog_path)
|
@@ -198,4 +200,18 @@ module IDRAC
|
|
198
200
|
end
|
199
201
|
end
|
200
202
|
|
201
|
-
|
203
|
+
# Create a separate CLI class for commands that don't require a host
|
204
|
+
module IDRAC
|
205
|
+
class StandaloneCLI < Thor
|
206
|
+
# Register the catalog command
|
207
|
+
desc "catalog", "Download Dell firmware catalog"
|
208
|
+
subcommand "catalog", CatalogCommand
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Check if the first argument is 'catalog'
|
213
|
+
if ARGV[0] == 'catalog'
|
214
|
+
IDRAC::StandaloneCLI.start(ARGV)
|
215
|
+
else
|
216
|
+
IDRAC::CLI.start(ARGV)
|
217
|
+
end
|
data/lib/idrac/error.rb
ADDED
data/lib/idrac/firmware.rb
CHANGED
@@ -5,6 +5,7 @@ require 'json'
|
|
5
5
|
require 'nokogiri'
|
6
6
|
require 'fileutils'
|
7
7
|
require 'securerandom'
|
8
|
+
require_relative 'firmware_catalog'
|
8
9
|
|
9
10
|
module IDRAC
|
10
11
|
class Firmware
|
@@ -22,6 +23,9 @@ module IDRAC
|
|
22
23
|
raise Error, "Firmware file not found: #{firmware_path}"
|
23
24
|
end
|
24
25
|
|
26
|
+
# Ensure we have a client
|
27
|
+
raise Error, "Client is required for firmware update" unless client
|
28
|
+
|
25
29
|
# Login to iDRAC
|
26
30
|
client.login unless client.instance_variable_get(:@session_id)
|
27
31
|
|
@@ -37,43 +41,15 @@ module IDRAC
|
|
37
41
|
end
|
38
42
|
|
39
43
|
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
|
44
|
+
# Use the new FirmwareCatalog class
|
45
|
+
catalog = FirmwareCatalog.new
|
46
|
+
catalog.download(output_dir)
|
74
47
|
end
|
75
48
|
|
76
49
|
def get_system_inventory
|
50
|
+
# Ensure we have a client
|
51
|
+
raise Error, "Client is required for system inventory" unless client
|
52
|
+
|
77
53
|
puts "Retrieving system inventory..."
|
78
54
|
|
79
55
|
# Get basic system information
|
@@ -133,148 +109,112 @@ module IDRAC
|
|
133
109
|
end
|
134
110
|
|
135
111
|
def check_updates(catalog_path = nil)
|
112
|
+
# Ensure we have a client for system inventory
|
113
|
+
raise Error, "Client is required for checking updates" unless client
|
114
|
+
|
136
115
|
# Download catalog if not provided
|
137
116
|
catalog_path ||= download_catalog
|
138
117
|
|
139
118
|
# Get system inventory
|
140
119
|
inventory = get_system_inventory
|
141
120
|
|
142
|
-
#
|
143
|
-
|
121
|
+
# Create a FirmwareCatalog instance
|
122
|
+
catalog = FirmwareCatalog.new(catalog_path)
|
144
123
|
|
145
|
-
# Extract
|
124
|
+
# Extract system information
|
125
|
+
system_model = inventory[:system][:model]
|
146
126
|
service_tag = inventory[:system][:service_tag]
|
147
127
|
|
148
128
|
puts "Checking updates for system with service tag: #{service_tag}"
|
129
|
+
puts "Searching for updates for model: #{system_model}"
|
149
130
|
|
150
|
-
# Find
|
151
|
-
|
131
|
+
# Find system models in the catalog
|
132
|
+
models = catalog.find_system_models(system_model.split.first)
|
152
133
|
|
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
|
-
}
|
134
|
+
if models.empty?
|
135
|
+
puts "No matching system model found in catalog"
|
136
|
+
return []
|
163
137
|
end
|
164
138
|
|
165
|
-
#
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
139
|
+
# Use the first matching model
|
140
|
+
model = models.first
|
141
|
+
puts "Found system IDs for #{model[:name]}: #{model[:id]}"
|
142
|
+
|
143
|
+
# Find updates for this system
|
144
|
+
catalog_updates = catalog.find_updates_for_system(model[:id])
|
145
|
+
puts "Found #{catalog_updates.size} firmware updates for #{model[:name]}"
|
146
|
+
|
147
|
+
# Compare current firmware with available updates
|
148
|
+
updates = []
|
149
|
+
|
150
|
+
# Print header for firmware comparison table
|
151
|
+
puts "\nFirmware Version Comparison:"
|
152
|
+
puts "=" * 100
|
153
|
+
puts "%-30s %-20s %-20s %-10s %-15s %-20s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
|
154
|
+
puts "-" * 100
|
155
|
+
|
156
|
+
# Process each firmware component
|
157
|
+
inventory[:firmware].each do |fw|
|
158
|
+
# Find matching updates in catalog
|
159
|
+
matching_updates = catalog_updates.select do |update|
|
160
|
+
catalog.match_component(fw[:name], update[:name])
|
161
|
+
end
|
180
162
|
|
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
|
163
|
+
if matching_updates.any?
|
164
|
+
# Use the first matching update
|
165
|
+
update = matching_updates.first
|
191
166
|
|
192
|
-
#
|
193
|
-
|
194
|
-
match_found = true
|
195
|
-
end
|
196
|
-
|
197
|
-
# 2. Check if BIOS components match
|
198
|
-
if (catalog_name.include?("bios") && firmware_name.include?("bios"))
|
199
|
-
match_found = true
|
200
|
-
end
|
167
|
+
# Check if version is newer
|
168
|
+
needs_update = catalog.compare_versions(fw[:version], update[:version])
|
201
169
|
|
202
|
-
#
|
203
|
-
if
|
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
|
209
|
-
|
210
|
-
# If we found a match and versions differ, add to updates
|
211
|
-
if match_found && !matched && version != fw_info[:version]
|
170
|
+
# Add to updates list if needed
|
171
|
+
if needs_update && fw[:updateable]
|
212
172
|
updates << {
|
213
|
-
name: name,
|
214
|
-
current_version:
|
215
|
-
available_version: version,
|
216
|
-
path: path,
|
217
|
-
component_type: component_type,
|
218
|
-
|
173
|
+
name: fw[:name],
|
174
|
+
current_version: fw[:version],
|
175
|
+
available_version: update[:version],
|
176
|
+
path: update[:path],
|
177
|
+
component_type: update[:component_type],
|
178
|
+
category: update[:category],
|
179
|
+
download_url: update[:download_url]
|
219
180
|
}
|
220
181
|
|
221
|
-
#
|
222
|
-
|
182
|
+
# Print row with update available
|
183
|
+
puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
|
184
|
+
fw[:name][0..29],
|
185
|
+
fw[:version],
|
186
|
+
update[:version],
|
187
|
+
fw[:updateable] ? "Yes" : "No",
|
188
|
+
update[:category] || "N/A",
|
189
|
+
"UPDATE AVAILABLE"
|
190
|
+
]
|
191
|
+
else
|
192
|
+
# Print row with no update available
|
193
|
+
puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
|
194
|
+
fw[:name][0..29],
|
195
|
+
fw[:version],
|
196
|
+
update[:version] || "N/A",
|
197
|
+
fw[:updateable] ? "Yes" : "No",
|
198
|
+
update[:category] || "N/A",
|
199
|
+
"No update available"
|
200
|
+
]
|
223
201
|
end
|
202
|
+
else
|
203
|
+
# No matching update found
|
204
|
+
puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
|
205
|
+
fw[:name][0..29],
|
206
|
+
fw[:version],
|
207
|
+
"N/A",
|
208
|
+
fw[:updateable] ? "Yes" : "No",
|
209
|
+
"N/A",
|
210
|
+
"No update available"
|
211
|
+
]
|
224
212
|
end
|
225
213
|
end
|
226
214
|
|
227
215
|
updates
|
228
216
|
end
|
229
217
|
|
230
|
-
def extract_identifiers(name)
|
231
|
-
return [] unless name
|
232
|
-
|
233
|
-
identifiers = []
|
234
|
-
|
235
|
-
# Extract model numbers like X520, I350, etc.
|
236
|
-
model_matches = name.scan(/[IX]\d{3,4}/)
|
237
|
-
identifiers.concat(model_matches)
|
238
|
-
|
239
|
-
# Extract PERC model like H730
|
240
|
-
perc_matches = name.scan(/[HP]\d{3,4}/)
|
241
|
-
identifiers.concat(perc_matches)
|
242
|
-
|
243
|
-
# Extract other common identifiers
|
244
|
-
if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
|
245
|
-
identifiers << "NIC"
|
246
|
-
end
|
247
|
-
|
248
|
-
if name.include?("PERC") || name.include?("RAID")
|
249
|
-
identifiers << "PERC"
|
250
|
-
# Extract PERC model like H730
|
251
|
-
perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
|
252
|
-
identifiers << perc_match[1] if perc_match
|
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"
|
269
|
-
end
|
270
|
-
|
271
|
-
if name.include?("CPLD")
|
272
|
-
identifiers << "CPLD"
|
273
|
-
end
|
274
|
-
|
275
|
-
identifiers
|
276
|
-
end
|
277
|
-
|
278
218
|
def interactive_update(catalog_path = nil)
|
279
219
|
updates = check_updates(catalog_path)
|
280
220
|
|
@@ -0,0 +1,243 @@
|
|
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 just the model number for PowerEdge servers (e.g., R640 from PowerEdge R640)
|
66
|
+
model_code = nil
|
67
|
+
if model_name.include?("PowerEdge")
|
68
|
+
model_code = model_name.split.last
|
69
|
+
end
|
70
|
+
|
71
|
+
doc.xpath('//SupportedSystems/Brand/Model').each do |model|
|
72
|
+
name = model.at_xpath('Display')&.text
|
73
|
+
code = model.at_xpath('Code')&.text
|
74
|
+
|
75
|
+
# Try to match by full name
|
76
|
+
if name && name.downcase.include?(model_name.downcase)
|
77
|
+
models << {
|
78
|
+
name: name,
|
79
|
+
code: code,
|
80
|
+
id: model['id']
|
81
|
+
}
|
82
|
+
# Try to match by model code (e.g., R640)
|
83
|
+
elsif model_code && code && code.downcase == model_code.downcase
|
84
|
+
models << {
|
85
|
+
name: name,
|
86
|
+
code: code,
|
87
|
+
id: model['id']
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
models
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_updates_for_system(system_id)
|
96
|
+
doc = parse
|
97
|
+
updates = []
|
98
|
+
|
99
|
+
doc.xpath("//SoftwareComponent[SupportedSystems/Brand/Model[@id='#{system_id}']]").each do |component|
|
100
|
+
name = component.at_xpath('Name')&.text
|
101
|
+
version = component.at_xpath('Version')&.text
|
102
|
+
path = component.at_xpath('Path')&.text
|
103
|
+
component_type = component.at_xpath('ComponentType')&.text
|
104
|
+
category = component.at_xpath('Category')&.text
|
105
|
+
|
106
|
+
next unless name && version && path
|
107
|
+
|
108
|
+
updates << {
|
109
|
+
name: name,
|
110
|
+
version: version,
|
111
|
+
path: path,
|
112
|
+
component_type: component_type,
|
113
|
+
category: category,
|
114
|
+
download_url: "https://downloads.dell.com/#{path}"
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
updates
|
119
|
+
end
|
120
|
+
|
121
|
+
def extract_identifiers(name)
|
122
|
+
return [] unless name
|
123
|
+
|
124
|
+
identifiers = []
|
125
|
+
|
126
|
+
# Extract model numbers like X520, I350, etc.
|
127
|
+
model_matches = name.scan(/[IX]\d{3,4}/)
|
128
|
+
identifiers.concat(model_matches)
|
129
|
+
|
130
|
+
# Extract PERC model like H730
|
131
|
+
perc_matches = name.scan(/[HP]\d{3,4}/)
|
132
|
+
identifiers.concat(perc_matches)
|
133
|
+
|
134
|
+
# Extract other common identifiers
|
135
|
+
if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
|
136
|
+
identifiers << "NIC"
|
137
|
+
end
|
138
|
+
|
139
|
+
if name.include?("PERC") || name.include?("RAID")
|
140
|
+
identifiers << "PERC"
|
141
|
+
# Extract PERC model like H730
|
142
|
+
perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
|
143
|
+
identifiers << perc_match[1] if perc_match
|
144
|
+
end
|
145
|
+
|
146
|
+
if name.include?("BIOS")
|
147
|
+
identifiers << "BIOS"
|
148
|
+
end
|
149
|
+
|
150
|
+
if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
|
151
|
+
identifiers << "iDRAC"
|
152
|
+
end
|
153
|
+
|
154
|
+
if name.include?("Power Supply") || name.include?("PSU")
|
155
|
+
identifiers << "PSU"
|
156
|
+
end
|
157
|
+
|
158
|
+
if name.include?("Lifecycle Controller")
|
159
|
+
identifiers << "LC"
|
160
|
+
end
|
161
|
+
|
162
|
+
if name.include?("CPLD")
|
163
|
+
identifiers << "CPLD"
|
164
|
+
end
|
165
|
+
|
166
|
+
identifiers
|
167
|
+
end
|
168
|
+
|
169
|
+
def match_component(firmware_name, catalog_name)
|
170
|
+
# Normalize names for comparison
|
171
|
+
catalog_name_lower = catalog_name.downcase.strip
|
172
|
+
firmware_name_lower = firmware_name.downcase.strip
|
173
|
+
|
174
|
+
# 1. Direct substring match
|
175
|
+
return true if catalog_name_lower.include?(firmware_name_lower) || firmware_name_lower.include?(catalog_name_lower)
|
176
|
+
|
177
|
+
# 2. Special case for BIOS
|
178
|
+
return true if catalog_name_lower.include?("bios") && firmware_name_lower.include?("bios")
|
179
|
+
|
180
|
+
# 3. Check identifiers
|
181
|
+
firmware_identifiers = extract_identifiers(firmware_name)
|
182
|
+
catalog_identifiers = extract_identifiers(catalog_name)
|
183
|
+
|
184
|
+
return true if (firmware_identifiers & catalog_identifiers).any?
|
185
|
+
|
186
|
+
# 4. Special case for network adapters
|
187
|
+
if (firmware_name_lower.include?("ethernet") || firmware_name_lower.include?("network")) &&
|
188
|
+
(catalog_name_lower.include?("ethernet") || catalog_name_lower.include?("network"))
|
189
|
+
return true
|
190
|
+
end
|
191
|
+
|
192
|
+
# No match found
|
193
|
+
false
|
194
|
+
end
|
195
|
+
|
196
|
+
def compare_versions(current_version, available_version)
|
197
|
+
# If versions are identical, no update needed
|
198
|
+
return false if current_version == available_version
|
199
|
+
|
200
|
+
# If either version is N/A, no update available
|
201
|
+
return false if current_version == "N/A" || available_version == "N/A"
|
202
|
+
|
203
|
+
# Try to handle Dell's version format (e.g., A00, A01, etc.)
|
204
|
+
if available_version.match?(/^[A-Z]\d+$/)
|
205
|
+
# If current version doesn't match Dell's format, assume update is needed
|
206
|
+
return true unless current_version.match?(/^[A-Z]\d+$/)
|
207
|
+
|
208
|
+
# Compare Dell version format (A00 < A01 < A02 < ... < B00 < B01 ...)
|
209
|
+
available_letter = available_version[0]
|
210
|
+
available_number = available_version[1..-1].to_i
|
211
|
+
|
212
|
+
current_letter = current_version[0]
|
213
|
+
current_number = current_version[1..-1].to_i
|
214
|
+
|
215
|
+
return true if current_letter < available_letter
|
216
|
+
return true if current_letter == available_letter && current_number < available_number
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
|
220
|
+
# For numeric versions, try to compare them
|
221
|
+
if current_version.match?(/^[\d\.]+$/) && available_version.match?(/^[\d\.]+$/)
|
222
|
+
current_parts = current_version.split('.').map(&:to_i)
|
223
|
+
available_parts = available_version.split('.').map(&:to_i)
|
224
|
+
|
225
|
+
# Compare each part of the version
|
226
|
+
max_length = [current_parts.length, available_parts.length].max
|
227
|
+
max_length.times do |i|
|
228
|
+
current_part = current_parts[i] || 0
|
229
|
+
available_part = available_parts[i] || 0
|
230
|
+
|
231
|
+
return true if current_part < available_part
|
232
|
+
return false if current_part > available_part
|
233
|
+
end
|
234
|
+
|
235
|
+
# If we get here, versions are equal
|
236
|
+
return false
|
237
|
+
end
|
238
|
+
|
239
|
+
# If we can't determine, assume update is needed
|
240
|
+
true
|
241
|
+
end
|
242
|
+
end
|
243
|
+
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
|
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.28
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
@@ -209,7 +209,9 @@ 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
|