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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2e790b22ca375a798916fdb6d908583d2fa4823e90a814c2e7c27796c7b6351
4
- data.tar.gz: 0ab7103ae540892dc959d22c7fa646050fe8eeaf003f6dc538c753fc3d614452
3
+ metadata.gz: 180bec6c43d2aced138c9479bbbfe23cec04c6149d2cd66343b5a738c0ce479f
4
+ data.tar.gz: 10fae4fc88a7d1ba456e3f7dc7d2efbfb8c7500b7839e7b59a2e27d5dae53c7c
5
5
  SHA512:
6
- metadata.gz: 89b75cdaf04c90368379af12884e55dcd4e5daa52ccda59143776934c871a60c039891a13194a52496532159bad0859f3b8502fe2f8beeaf433319ba1d5aa0e1
7
- data.tar.gz: 6dee9f26c047f9931ea846e2eaf738f91510801fdb090c384d18fd76639ca098db11ca071acb55ab81a0c319bb42ecb781d33f71ed83c66c9dac22c58c8bf2c6
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 firmware:catalog --host=192.168.1.100 --username=root --password=calvin
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
- catalog_path = firmware.download_catalog
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
- check_ssl_verification
46
- client = create_client
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 firmware:catalog'"
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
- catalog_path = firmware.download_catalog
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
- IDRAC::CLI.start(ARGV)
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
- # Use Basic Auth for the initial session creation
134
- basic_auth_headers = {
135
- 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
136
- 'Content-Type' => 'application/json'
137
- }
138
-
139
- response = connection.post(url) do |req|
140
- req.headers.merge!(basic_auth_headers)
141
- req.body = payload.to_json
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
- if response.status == 201 || response.status == 200
145
- # Extract X-Auth-Token from response headers
146
- @x_auth_token = response.headers['X-Auth-Token']
147
-
148
- # Extract session location from response headers
149
- @session_location = response.headers['Location']
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
- puts "Redfish session created successfully"
152
- @sessions_maxed = false
153
- return true
154
- elsif response.status == 400 && response.body.include?("maximum number of user sessions")
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
- # Try to clear sessions if auto_delete_sessions is enabled
159
- if @auto_delete_sessions
160
- puts "Auto-delete sessions is enabled, attempting to clear sessions"
161
- if force_clear_sessions
162
- puts "Successfully cleared sessions, trying to create a new session"
163
-
164
- # Try one more time after clearing
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.merge!(basic_auth_headers)
167
- req.body = payload.to_json
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 after clearing sessions"
231
+ puts "Redfish session created successfully with form-urlencoded"
174
232
  @sessions_maxed = false
175
233
  return true
176
234
  else
177
- puts "Failed to create Redfish session after clearing: #{response.status} - #{response.body}"
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
- else
193
- puts "Failed to create Redfish session: #{response.status} - #{response.body}"
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
@@ -0,0 +1,3 @@
1
+ module IDRAC
2
+ class Error < StandardError; end
3
+ end
@@ -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 ~/.idrac as the default directory
41
- output_dir ||= File.expand_path("~/.idrac")
42
- FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
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
- # Parse catalog
143
- catalog_doc = File.open(catalog_path) { |f| Nokogiri::XML(f) }
121
+ # Create a FirmwareCatalog instance
122
+ catalog = FirmwareCatalog.new(catalog_path)
144
123
 
145
- # Extract service tag
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 applicable updates
151
- updates = []
131
+ # Find system models in the catalog
132
+ models = catalog.find_system_models(system_model.split.first)
152
133
 
153
- # Get current firmware versions
154
- current_versions = {}
155
- inventory[:firmware].each do |fw|
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
- # Find matching components in catalog
165
- catalog_doc.xpath('//SoftwareComponent').each do |component|
166
- name = component.at_xpath('Name')&.text
167
- version = component.at_xpath('Version')&.text
168
- path = component.at_xpath('Path')&.text
169
- component_type = component.at_xpath('ComponentType')&.text
170
-
171
- next unless name && version && path # Skip if missing essential data
172
-
173
- # Check if this component matches any of our firmware
174
- # We'll track if we found a match to avoid duplicates
175
- matched = false
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
- current_versions.each do |id, fw_info|
178
- # Normalize names for comparison
179
- catalog_name = name.downcase.strip
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 names match or contain each other
183
- if catalog_name.include?(firmware_name) || firmware_name.include?(catalog_name) ||
184
- (catalog_name.include?("bios") && firmware_name.include?("bios"))
185
-
186
- # Skip if already matched to avoid duplicates
187
- next if matched
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
- # Compare versions
190
- if version != fw_info[:version] && fw_info[:updateable]
191
- updates << {
192
- name: name,
193
- current_version: fw_info[:version],
194
- available_version: version,
195
- path: path,
196
- component_type: component_type,
197
- download_url: "https://downloads.dell.com/#{path}"
198
- }
199
-
200
- # Mark as matched to avoid duplicates
201
- matched = true
202
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.1.24"
4
+ VERSION = "0.1.28"
5
5
  end
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(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true)
21
- Client.new(
22
- host: host,
23
- username: username,
24
- password: password,
25
- port: port,
26
- use_ssl: use_ssl,
27
- verify_ssl: verify_ssl
28
- )
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.24
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