idrac 0.1.24 → 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 +36 -4
- data/bin/idrac +38 -22
- data/lib/idrac/client.rb +93 -40
- data/lib/idrac/error.rb +3 -0
- data/lib/idrac/firmware.rb +92 -84
- 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,35 @@ 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
|
+
|
148
|
+
### Version 0.1.26
|
149
|
+
- **Improved Redfish Session Creation**: Fixed issues with the Redfish session creation process
|
150
|
+
- Added multiple fallback methods for creating sessions with different iDRAC versions
|
151
|
+
- Fixed 415 Unsupported Media Type error by trying different content types
|
152
|
+
- Added support for form-urlencoded requests when JSON requests fail
|
153
|
+
- Enhanced error handling and logging during session creation
|
154
|
+
|
155
|
+
### Version 0.1.25
|
156
|
+
- **Enhanced Component Matching**: Improved firmware component matching with catalog entries
|
157
|
+
- Added extraction of model numbers and identifiers from component names (X520, H730, etc.)
|
158
|
+
- Implemented multiple matching strategies for better accuracy
|
159
|
+
- Added special handling for different component types (NIC, PERC, BIOS, iDRAC, etc.)
|
160
|
+
- Improved matching for components with different naming conventions
|
161
|
+
|
130
162
|
### Version 0.1.24
|
131
163
|
- **Improved Firmware Update Check**: Fixed issues with firmware version comparison
|
132
164
|
- Eliminated duplicate entries in firmware update results
|
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/client.rb
CHANGED
@@ -130,67 +130,120 @@ module IDRAC
|
|
130
130
|
url = '/redfish/v1/SessionService/Sessions'
|
131
131
|
payload = { "UserName" => username, "Password" => password }
|
132
132
|
|
133
|
-
#
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
133
|
+
# Try first with just Content-Type header (no Basic Auth)
|
134
|
+
begin
|
135
|
+
response = connection.post(url) do |req|
|
136
|
+
req.headers['Content-Type'] = 'application/json'
|
137
|
+
req.body = payload.to_json
|
138
|
+
end
|
139
|
+
|
140
|
+
if response.status == 201 || response.status == 200
|
141
|
+
# Extract X-Auth-Token from response headers
|
142
|
+
@x_auth_token = response.headers['X-Auth-Token']
|
143
|
+
|
144
|
+
# Extract session location from response headers
|
145
|
+
@session_location = response.headers['Location']
|
146
|
+
|
147
|
+
puts "Redfish session created successfully"
|
148
|
+
@sessions_maxed = false
|
149
|
+
return true
|
150
|
+
end
|
151
|
+
rescue => e
|
152
|
+
puts "First session creation attempt failed: #{e.message}"
|
142
153
|
end
|
143
154
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
155
|
+
# If that fails, try with Basic Auth
|
156
|
+
begin
|
157
|
+
# Use Basic Auth for the session creation
|
158
|
+
basic_auth_headers = {
|
159
|
+
'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
|
160
|
+
'Content-Type' => 'application/json'
|
161
|
+
}
|
150
162
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
puts "Maximum sessions reached during Redfish session creation"
|
156
|
-
@sessions_maxed = true
|
163
|
+
response = connection.post(url) do |req|
|
164
|
+
req.headers.merge!(basic_auth_headers)
|
165
|
+
req.body = payload.to_json
|
166
|
+
end
|
157
167
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
168
|
+
if response.status == 201 || response.status == 200
|
169
|
+
# Extract X-Auth-Token from response headers
|
170
|
+
@x_auth_token = response.headers['X-Auth-Token']
|
171
|
+
|
172
|
+
# Extract session location from response headers
|
173
|
+
@session_location = response.headers['Location']
|
174
|
+
|
175
|
+
puts "Redfish session created successfully with Basic Auth"
|
176
|
+
@sessions_maxed = false
|
177
|
+
return true
|
178
|
+
elsif response.status == 400 && response.body.include?("maximum number of user sessions")
|
179
|
+
puts "Maximum sessions reached during Redfish session creation"
|
180
|
+
@sessions_maxed = true
|
181
|
+
|
182
|
+
# Try to clear sessions if auto_delete_sessions is enabled
|
183
|
+
if @auto_delete_sessions
|
184
|
+
puts "Auto-delete sessions is enabled, attempting to clear sessions"
|
185
|
+
if force_clear_sessions
|
186
|
+
puts "Successfully cleared sessions, trying to create a new session"
|
187
|
+
|
188
|
+
# Try one more time after clearing
|
189
|
+
response = connection.post(url) do |req|
|
190
|
+
req.headers.merge!(basic_auth_headers)
|
191
|
+
req.body = payload.to_json
|
192
|
+
end
|
193
|
+
|
194
|
+
if response.status == 201 || response.status == 200
|
195
|
+
@x_auth_token = response.headers['X-Auth-Token']
|
196
|
+
@session_location = response.headers['Location']
|
197
|
+
puts "Redfish session created successfully after clearing sessions"
|
198
|
+
@sessions_maxed = false
|
199
|
+
return true
|
200
|
+
else
|
201
|
+
puts "Failed to create Redfish session after clearing: #{response.status} - #{response.body}"
|
202
|
+
# If we still can't create a session, switch to direct mode
|
203
|
+
@direct_mode = true
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
else
|
207
|
+
puts "Failed to clear sessions, switching to direct mode"
|
208
|
+
@direct_mode = true
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
else
|
212
|
+
puts "Auto-delete sessions is disabled, switching to direct mode"
|
213
|
+
@direct_mode = true
|
214
|
+
return false
|
215
|
+
end
|
216
|
+
else
|
217
|
+
puts "Failed to create Redfish session: #{response.status} - #{response.body}"
|
218
|
+
|
219
|
+
# If we get a 415 error, try with form-urlencoded
|
220
|
+
if response.status == 415
|
221
|
+
puts "Trying with form-urlencoded content type"
|
165
222
|
response = connection.post(url) do |req|
|
166
|
-
req.headers
|
167
|
-
req.
|
223
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
224
|
+
req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
225
|
+
req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
|
168
226
|
end
|
169
227
|
|
170
228
|
if response.status == 201 || response.status == 200
|
171
229
|
@x_auth_token = response.headers['X-Auth-Token']
|
172
230
|
@session_location = response.headers['Location']
|
173
|
-
puts "Redfish session created successfully
|
231
|
+
puts "Redfish session created successfully with form-urlencoded"
|
174
232
|
@sessions_maxed = false
|
175
233
|
return true
|
176
234
|
else
|
177
|
-
puts "Failed
|
178
|
-
# If we still can't create a session, switch to direct mode
|
235
|
+
puts "Failed with form-urlencoded too: #{response.status} - #{response.body}"
|
179
236
|
@direct_mode = true
|
180
237
|
return false
|
181
238
|
end
|
182
239
|
else
|
183
|
-
puts "Failed to clear sessions, switching to direct mode"
|
184
240
|
@direct_mode = true
|
185
241
|
return false
|
186
242
|
end
|
187
|
-
else
|
188
|
-
puts "Auto-delete sessions is disabled, switching to direct mode"
|
189
|
-
@direct_mode = true
|
190
|
-
return false
|
191
243
|
end
|
192
|
-
|
193
|
-
puts "
|
244
|
+
rescue => e
|
245
|
+
puts "Error during Redfish session creation: #{e.message}"
|
246
|
+
@direct_mode = true
|
194
247
|
return false
|
195
248
|
end
|
196
249
|
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,74 +109,106 @@ 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
|
-
}
|
134
|
+
if models.empty?
|
135
|
+
puts "No matching system model found in catalog"
|
136
|
+
return []
|
162
137
|
end
|
163
138
|
|
164
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
176
162
|
|
177
|
-
|
178
|
-
#
|
179
|
-
|
180
|
-
firmware_name = fw_info[:name].downcase.strip
|
163
|
+
if matching_updates.any?
|
164
|
+
# Use the first matching update
|
165
|
+
update = matching_updates.first
|
181
166
|
|
182
|
-
# Check if
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
167
|
+
# Check if version is newer
|
168
|
+
needs_update = catalog.compare_versions(fw[:version], update[:version])
|
169
|
+
|
170
|
+
# Add to updates list if needed
|
171
|
+
if needs_update && fw[:updateable]
|
172
|
+
updates << {
|
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]
|
180
|
+
}
|
188
181
|
|
189
|
-
#
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
+
]
|
203
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
|
+
]
|
204
212
|
end
|
205
213
|
end
|
206
214
|
|
@@ -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
|