idrac 0.1.28 → 0.1.30

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.
@@ -2,10 +2,13 @@ require 'net/http'
2
2
  require 'uri'
3
3
  require 'fileutils'
4
4
  require 'nokogiri'
5
+ require 'open-uri'
6
+ require 'colorize'
5
7
 
6
8
  module IDRAC
7
9
  class FirmwareCatalog
8
- CATALOG_URL = "https://downloads.dell.com/catalog/Catalog.xml.gz"
10
+ DELL_CATALOG_BASE = 'https://downloads.dell.com'
11
+ DELL_CATALOG_URL = "#{DELL_CATALOG_BASE}/catalog/Catalog.xml.gz"
9
12
 
10
13
  attr_reader :catalog_path
11
14
 
@@ -14,40 +17,38 @@ module IDRAC
14
17
  end
15
18
 
16
19
  def download(output_dir = nil)
17
- # Use ~/.idrac as the default directory
18
- output_dir ||= File.expand_path("~/.idrac")
20
+ # Default to ~/.idrac directory
21
+ output_dir ||= File.expand_path('~/.idrac')
19
22
  FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
20
23
 
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
24
+ catalog_gz = File.join(output_dir, 'Catalog.xml.gz')
25
+ catalog_xml = File.join(output_dir, 'Catalog.xml')
41
26
 
42
- puts "Extracting catalog..."
43
- system("gunzip -f #{catalog_gz_path}")
27
+ puts "Downloading Dell catalog from #{DELL_CATALOG_URL}...".light_cyan
44
28
 
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"
29
+ begin
30
+ # Download the catalog
31
+ URI.open(DELL_CATALOG_URL) do |remote_file|
32
+ File.open(catalog_gz, 'wb') do |local_file|
33
+ local_file.write(remote_file.read)
34
+ end
35
+ end
36
+
37
+ puts "Extracting catalog...".light_cyan
38
+
39
+ # Extract the catalog
40
+ system("gunzip -f #{catalog_gz}")
41
+
42
+ if File.exist?(catalog_xml)
43
+ puts "Catalog downloaded and extracted to #{catalog_xml}".green
44
+ @catalog_path = catalog_xml
45
+ return catalog_xml
46
+ else
47
+ raise Error, "Failed to extract catalog"
48
+ end
49
+ rescue => e
50
+ puts "Error downloading catalog: #{e.message}".red.bold
51
+ raise Error, "Failed to download Dell catalog: #{e.message}"
51
52
  end
52
53
  end
53
54
 
@@ -62,59 +63,130 @@ module IDRAC
62
63
  doc = parse
63
64
  models = []
64
65
 
65
- # Extract just the model number for PowerEdge servers (e.g., R640 from PowerEdge R640)
66
+ # Extract model code from full model name (e.g., "PowerEdge R640" -> "R640")
66
67
  model_code = nil
67
68
  if model_name.include?("PowerEdge")
68
69
  model_code = model_name.split.last
70
+ else
71
+ model_code = model_name
69
72
  end
70
73
 
74
+ puts "Searching for model: #{model_name} (code: #{model_code})"
75
+
76
+ # Build a mapping of model names to system IDs
77
+ model_to_system_id = {}
78
+
71
79
  doc.xpath('//SupportedSystems/Brand/Model').each do |model|
80
+ system_id = model['systemID'] || model['id']
72
81
  name = model.at_xpath('Display')&.text
73
82
  code = model.at_xpath('Code')&.text
74
83
 
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 << {
84
+ if name && system_id
85
+ model_to_system_id[name] = {
85
86
  name: name,
86
87
  code: code,
87
- id: model['id']
88
+ id: system_id
88
89
  }
90
+
91
+ # Also map just the model number (R640, etc.)
92
+ if name =~ /[RT]\d+/
93
+ model_short = name.match(/([RT]\d+\w*)/)[1]
94
+ model_to_system_id[model_short] = {
95
+ name: name,
96
+ code: code,
97
+ id: system_id
98
+ }
99
+ end
100
+ end
101
+ end
102
+
103
+ # Try exact match first
104
+ if model_to_system_id[model_name]
105
+ models << model_to_system_id[model_name]
106
+ end
107
+
108
+ # Try model code match
109
+ if model_to_system_id[model_code]
110
+ models << model_to_system_id[model_code]
111
+ end
112
+
113
+ # If we still don't have a match, try a more flexible approach
114
+ if models.empty?
115
+ model_to_system_id.each do |name, model_info|
116
+ if name.include?(model_code) || model_code.include?(name)
117
+ models << model_info
118
+ end
119
+ end
120
+ end
121
+
122
+ # If still no match, try matching by systemID directly
123
+ if models.empty?
124
+ doc.xpath('//SupportedSystems/Brand/Model').each do |model|
125
+ system_id = model['systemID'] || model['id']
126
+ name = model.at_xpath('Display')&.text
127
+ code = model.at_xpath('Code')&.text
128
+
129
+ if code && code.downcase == model_code.downcase
130
+ models << {
131
+ name: name,
132
+ code: code,
133
+ id: system_id
134
+ }
135
+ end
89
136
  end
90
137
  end
91
138
 
92
- models
139
+ models.uniq { |m| m[:id] }
93
140
  end
94
141
 
95
142
  def find_updates_for_system(system_id)
96
143
  doc = parse
97
144
  updates = []
98
145
 
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
146
+ # Find all SoftwareComponents
147
+ doc.xpath("//SoftwareComponent").each do |component|
148
+ # Check if this component supports our system ID
149
+ supported_system_ids = component.xpath(".//SupportedSystems/Brand/Model/@systemID | .//SupportedSystems/Brand/Model/@id").map(&:value)
150
+
151
+ next unless supported_system_ids.include?(system_id)
152
+
153
+ # Get component details
154
+ name_node = component.xpath("./Name/Display[@lang='en']").first
155
+ name = name_node ? name_node.text.strip : ""
156
+
157
+ component_type_node = component.xpath("./ComponentType/Display[@lang='en']").first
158
+ component_type = component_type_node ? component_type_node.text.strip : ""
159
+
160
+ path = component['path'] || ""
161
+ category_node = component.xpath("./Category/Display[@lang='en']").first
162
+ category = category_node ? category_node.text.strip : ""
105
163
 
106
- next unless name && version && path
164
+ version = component['dellVersion'] || component['vendorVersion'] || ""
107
165
 
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
- }
166
+ # Skip if missing essential information
167
+ next if name.empty? || path.empty? || version.empty?
168
+
169
+ # Only include firmware updates
170
+ if component_type.include?("Firmware") ||
171
+ category.include?("BIOS") ||
172
+ category.include?("Firmware") ||
173
+ category.include?("iDRAC") ||
174
+ name.include?("BIOS") ||
175
+ name.include?("Firmware") ||
176
+ name.include?("iDRAC")
177
+
178
+ updates << {
179
+ name: name,
180
+ version: version,
181
+ path: path,
182
+ component_type: component_type,
183
+ category: category,
184
+ download_url: "https://downloads.dell.com/#{path}"
185
+ }
186
+ end
116
187
  end
117
188
 
189
+ puts "Found #{updates.size} firmware updates for system ID #{system_id}"
118
190
  updates
119
191
  end
120
192
 
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.28"
4
+ VERSION = "0.1.30"
5
5
  end
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'lib/idrac'
4
+ require 'colorize'
5
+
6
+ # Create a client
7
+ client = IDRAC::Client.new(
8
+ host: '127.0.0.1',
9
+ username: 'root',
10
+ password: 'calvin',
11
+ verify_ssl: false
12
+ )
13
+
14
+ begin
15
+ # Login to iDRAC
16
+ puts "Logging in to iDRAC...".light_cyan
17
+ client.login
18
+ puts "Logged in successfully".green
19
+
20
+ # Create a firmware instance
21
+ firmware = IDRAC::Firmware.new(client)
22
+
23
+ # Get system inventory
24
+ puts "Getting system inventory...".light_cyan
25
+ inventory = firmware.get_system_inventory
26
+
27
+ puts "System Information:".green.bold
28
+ puts " Model: #{inventory[:system][:model]}".light_cyan
29
+ puts " Manufacturer: #{inventory[:system][:manufacturer]}".light_cyan
30
+ puts " Service Tag: #{inventory[:system][:service_tag]}".light_cyan
31
+ puts " BIOS Version: #{inventory[:system][:bios_version]}".light_cyan
32
+
33
+ puts "\nInstalled Firmware:".green.bold
34
+ inventory[:firmware].each do |fw|
35
+ puts " #{fw[:name]}: #{fw[:version]} (#{fw[:updateable] ? 'Updateable'.light_green : 'Not Updateable'.light_red})".light_cyan
36
+ end
37
+
38
+ # Check for updates
39
+ catalog_path = File.expand_path("~/.idrac/Catalog.xml")
40
+ if File.exist?(catalog_path)
41
+ puts "\nChecking for updates using catalog: #{catalog_path}".light_cyan
42
+ updates = firmware.check_updates(catalog_path)
43
+
44
+ if updates.any?
45
+ puts "\nAvailable Updates:".green.bold
46
+ updates.each_with_index do |update, index|
47
+ puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}".light_cyan
48
+ end
49
+
50
+ # Interactive update for the first update
51
+ puts "\nSelected update: #{updates.first[:name]}".light_yellow
52
+ puts "Starting interactive update for selected component...".light_cyan.bold
53
+
54
+ firmware.interactive_update(catalog_path, [updates.first])
55
+ else
56
+ puts "No updates available for your system.".yellow
57
+ end
58
+ else
59
+ puts "\nCatalog not found at #{catalog_path}. Run 'idrac firmware:catalog' to download it.".yellow
60
+ end
61
+
62
+ rescue IDRAC::Error => e
63
+ puts "Error: #{e.message}".red.bold
64
+ ensure
65
+ # Logout
66
+ client.logout if client
67
+ puts "Logged out".light_cyan
68
+ end