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.
- checksums.yaml +4 -4
- data/README.md +24 -1
- data/bin/idrac +80 -51
- data/dell_firmware_downloads/Catalog.etag +1 -0
- data/dell_firmware_downloads/Catalog.xml +0 -0
- data/idrac-0.1.6/.rspec +3 -0
- data/idrac-0.1.6/README.md +103 -0
- data/idrac-0.1.6/Rakefile +17 -0
- data/idrac-0.1.6/bin/console +11 -0
- data/idrac-0.1.6/bin/idrac +179 -0
- data/idrac-0.1.6/bin/setup +8 -0
- data/idrac-0.1.6/idrac.gemspec +51 -0
- data/idrac-0.1.6/lib/idrac/client.rb +109 -0
- data/idrac-0.1.6/lib/idrac/firmware.rb +366 -0
- data/idrac-0.1.6/lib/idrac/version.rb +5 -0
- data/idrac-0.1.6/lib/idrac.rb +30 -0
- data/idrac-0.1.6/sig/idrac.rbs +4 -0
- data/idrac-0.1.7/.rspec +3 -0
- data/idrac-0.1.7/README.md +103 -0
- data/idrac-0.1.7/Rakefile +17 -0
- data/idrac-0.1.7/bin/console +11 -0
- data/idrac-0.1.7/bin/idrac +179 -0
- data/idrac-0.1.7/bin/setup +8 -0
- data/idrac-0.1.7/idrac.gemspec +51 -0
- data/idrac-0.1.7/lib/idrac/client.rb +109 -0
- data/idrac-0.1.7/lib/idrac/firmware.rb +366 -0
- data/idrac-0.1.7/lib/idrac/screenshot.rb +49 -0
- data/idrac-0.1.7/lib/idrac/version.rb +5 -0
- data/idrac-0.1.7/lib/idrac.rb +30 -0
- data/idrac-0.1.7/sig/idrac.rbs +4 -0
- data/idrac.gemspec +1 -0
- data/idrac.py +500 -0
- data/lib/idrac/client.rb +206 -144
- data/lib/idrac/firmware.rb +272 -62
- data/lib/idrac/firmware_catalog.rb +131 -59
- data/lib/idrac/version.rb +1 -1
- data/test_firmware_update.rb +68 -0
- data/updater.rb +729 -0
- metadata +45 -1
@@ -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
|
-
|
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
|
-
#
|
18
|
-
output_dir ||= File.expand_path(
|
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
|
-
|
22
|
-
|
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 "
|
43
|
-
system("gunzip -f #{catalog_gz_path}")
|
27
|
+
puts "Downloading Dell catalog from #{DELL_CATALOG_URL}...".light_cyan
|
44
28
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
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
|
-
|
76
|
-
|
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:
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
164
|
+
version = component['dellVersion'] || component['vendorVersion'] || ""
|
107
165
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
@@ -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
|