idrac 0.1.31 → 0.1.38

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.
@@ -1,366 +0,0 @@
1
- require 'tempfile'
2
- require 'net/http'
3
- require 'uri'
4
- require 'json'
5
- require 'nokogiri'
6
- require 'fileutils'
7
- require 'securerandom'
8
-
9
- module IDRAC
10
- class Firmware
11
- attr_reader :client
12
-
13
- CATALOG_URL = "https://downloads.dell.com/catalog/Catalog.xml.gz"
14
-
15
- def initialize(client)
16
- @client = client
17
- end
18
-
19
- def update(firmware_path, options = {})
20
- # Validate firmware file exists
21
- unless File.exist?(firmware_path)
22
- raise Error, "Firmware file not found: #{firmware_path}"
23
- end
24
-
25
- # Login to iDRAC
26
- client.login unless client.instance_variable_get(:@session_id)
27
-
28
- # Upload firmware file
29
- job_id = upload_firmware(firmware_path)
30
-
31
- # Check if we should wait for the update to complete
32
- if options[:wait]
33
- wait_for_job_completion(job_id, options[:timeout] || 3600)
34
- end
35
-
36
- job_id
37
- end
38
-
39
- def download_catalog(output_dir = nil)
40
- output_dir ||= Dir.pwd
41
- FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
42
-
43
- catalog_gz_path = File.join(output_dir, "Catalog.xml.gz")
44
- catalog_path = File.join(output_dir, "Catalog.xml")
45
-
46
- puts "Downloading Dell catalog from #{CATALOG_URL}..."
47
-
48
- uri = URI.parse(CATALOG_URL)
49
- Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
50
- request = Net::HTTP::Get.new(uri)
51
- http.request(request) do |response|
52
- if response.code == "200"
53
- File.open(catalog_gz_path, 'wb') do |file|
54
- response.read_body do |chunk|
55
- file.write(chunk)
56
- end
57
- end
58
- else
59
- raise Error, "Failed to download catalog: #{response.code} #{response.message}"
60
- end
61
- end
62
- end
63
-
64
- puts "Extracting catalog..."
65
- system("gunzip -f #{catalog_gz_path}")
66
-
67
- if File.exist?(catalog_path)
68
- puts "Catalog downloaded and extracted to #{catalog_path}"
69
- return catalog_path
70
- else
71
- raise Error, "Failed to extract catalog"
72
- end
73
- end
74
-
75
- def get_system_inventory
76
- puts "Retrieving system inventory..."
77
-
78
- # Get basic system information
79
- system_uri = URI.parse("#{client.base_url}/redfish/v1/Systems/System.Embedded.1")
80
- system_response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
81
-
82
- if system_response.status != 200
83
- raise Error, "Failed to get system information: #{system_response.status}"
84
- end
85
-
86
- system_data = JSON.parse(system_response.body)
87
-
88
- # Get firmware inventory
89
- firmware_uri = URI.parse("#{client.base_url}/redfish/v1/UpdateService/FirmwareInventory")
90
- firmware_response = client.authenticated_request(:get, "/redfish/v1/UpdateService/FirmwareInventory")
91
-
92
- if firmware_response.status != 200
93
- raise Error, "Failed to get firmware inventory: #{firmware_response.status}"
94
- end
95
-
96
- firmware_data = JSON.parse(firmware_response.body)
97
-
98
- # Get detailed firmware information for each component
99
- firmware_inventory = []
100
-
101
- if firmware_data['Members'] && firmware_data['Members'].is_a?(Array)
102
- firmware_data['Members'].each do |member|
103
- if member['@odata.id']
104
- component_uri = member['@odata.id']
105
- component_response = client.authenticated_request(:get, component_uri)
106
-
107
- if component_response.status == 200
108
- component_data = JSON.parse(component_response.body)
109
- firmware_inventory << {
110
- name: component_data['Name'],
111
- id: component_data['Id'],
112
- version: component_data['Version'],
113
- updateable: component_data['Updateable'] || false,
114
- status: component_data['Status'] ? component_data['Status']['State'] : 'Unknown'
115
- }
116
- end
117
- end
118
- end
119
- end
120
-
121
- {
122
- system: {
123
- model: system_data['Model'],
124
- manufacturer: system_data['Manufacturer'],
125
- serial_number: system_data['SerialNumber'],
126
- part_number: system_data['PartNumber'],
127
- bios_version: system_data['BiosVersion'],
128
- service_tag: system_data['SKU']
129
- },
130
- firmware: firmware_inventory
131
- }
132
- end
133
-
134
- def check_updates(catalog_path = nil)
135
- # Download catalog if not provided
136
- catalog_path ||= download_catalog
137
-
138
- # Get system inventory
139
- inventory = get_system_inventory
140
-
141
- # Parse catalog
142
- catalog_doc = File.open(catalog_path) { |f| Nokogiri::XML(f) }
143
-
144
- # Extract service tag
145
- service_tag = inventory[:system][:service_tag]
146
-
147
- puts "Checking updates for system with service tag: #{service_tag}"
148
-
149
- # Find applicable updates
150
- updates = []
151
-
152
- # Get current firmware versions
153
- current_versions = {}
154
- inventory[:firmware].each do |fw|
155
- current_versions[fw[:name]] = fw[:version]
156
- end
157
-
158
- # Find matching components in catalog
159
- catalog_doc.xpath('//SoftwareComponent').each do |component|
160
- name = component.at_xpath('Name')&.text
161
- version = component.at_xpath('Version')&.text
162
- path = component.at_xpath('Path')&.text
163
- component_type = component.at_xpath('ComponentType')&.text
164
-
165
- # Check if this component matches any of our firmware
166
- inventory[:firmware].each do |fw|
167
- if fw[:name].include?(name) || name.include?(fw[:name])
168
- current_version = fw[:version]
169
-
170
- # Simple version comparison (this could be improved)
171
- if version != current_version
172
- updates << {
173
- name: name,
174
- current_version: current_version,
175
- available_version: version,
176
- path: path,
177
- component_type: component_type,
178
- download_url: "https://downloads.dell.com/#{path}"
179
- }
180
- end
181
- end
182
- end
183
- end
184
-
185
- updates
186
- end
187
-
188
- def interactive_update(catalog_path = nil)
189
- updates = check_updates(catalog_path)
190
-
191
- if updates.empty?
192
- puts "No updates available for your system."
193
- return
194
- end
195
-
196
- puts "\nAvailable updates:"
197
- updates.each_with_index do |update, index|
198
- puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
199
- end
200
-
201
- puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):"
202
- choice = STDIN.gets.chomp
203
-
204
- return if choice.downcase == 'q'
205
-
206
- selected_updates = if choice.downcase == 'all'
207
- updates
208
- else
209
- index = choice.to_i - 1
210
- if index >= 0 && index < updates.size
211
- [updates[index]]
212
- else
213
- puts "Invalid selection."
214
- return
215
- end
216
- end
217
-
218
- selected_updates.each do |update|
219
- puts "Downloading #{update[:name]} version #{update[:available_version]}..."
220
-
221
- # Create temp directory
222
- temp_dir = Dir.mktmpdir
223
-
224
- begin
225
- # Download the update
226
- update_filename = File.basename(update[:path])
227
- update_path = File.join(temp_dir, update_filename)
228
-
229
- uri = URI.parse(update[:download_url])
230
- Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
231
- request = Net::HTTP::Get.new(uri)
232
- http.request(request) do |response|
233
- if response.code == "200"
234
- File.open(update_path, 'wb') do |file|
235
- response.read_body do |chunk|
236
- file.write(chunk)
237
- end
238
- end
239
- else
240
- puts "Failed to download update: #{response.code} #{response.message}"
241
- next
242
- end
243
- end
244
- end
245
-
246
- puts "Installing #{update[:name]} version #{update[:available_version]}..."
247
- job_id = update(update_path, wait: true)
248
- puts "Update completed with job ID: #{job_id}"
249
-
250
- ensure
251
- # Clean up temp directory
252
- FileUtils.remove_entry(temp_dir)
253
- end
254
- end
255
- end
256
-
257
- private
258
-
259
- def upload_firmware(firmware_path)
260
- puts "Uploading firmware file: #{firmware_path}"
261
-
262
- # Get the HttpPushUri from UpdateService
263
- update_service_response = client.authenticated_request(:get, "/redfish/v1/UpdateService")
264
- update_service_data = JSON.parse(update_service_response.body)
265
-
266
- http_push_uri = update_service_data['HttpPushUri']
267
- if http_push_uri.nil?
268
- http_push_uri = "/redfish/v1/UpdateService/FirmwareInventory"
269
- puts "HttpPushUri not found, using default: #{http_push_uri}"
270
- else
271
- puts "Found HttpPushUri: #{http_push_uri}"
272
- end
273
-
274
- # Get the ETag for the HttpPushUri
275
- etag_response = client.authenticated_request(:get, http_push_uri)
276
- etag = etag_response.headers['etag']
277
-
278
- puts "Got ETag: #{etag}"
279
-
280
- # Create a boundary for multipart/form-data
281
- boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
282
-
283
- # Read the file content
284
- file_content = File.binread(firmware_path)
285
- filename = File.basename(firmware_path)
286
-
287
- # Create the multipart body
288
- post_body = []
289
- post_body << "--#{boundary}\r\n"
290
- post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
291
- post_body << "Content-Type: application/octet-stream\r\n\r\n"
292
- post_body << file_content
293
- post_body << "\r\n--#{boundary}--\r\n"
294
-
295
- # Upload the firmware
296
- response = client.authenticated_request(
297
- :post,
298
- http_push_uri,
299
- {
300
- headers: {
301
- 'Content-Type' => "multipart/form-data; boundary=#{boundary}",
302
- 'If-Match' => etag
303
- },
304
- body: post_body.join
305
- }
306
- )
307
-
308
- if response.status < 200 || response.status >= 300
309
- raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
310
- end
311
-
312
- # Extract job ID from response
313
- response_data = JSON.parse(response.body)
314
- job_id = response_data['Id'] || response_data['TaskId']
315
-
316
- if job_id.nil?
317
- raise Error, "Failed to extract job ID from firmware upload response"
318
- end
319
-
320
- puts "Firmware update job created with ID: #{job_id}"
321
- job_id
322
- end
323
-
324
- def wait_for_job_completion(job_id, timeout)
325
- puts "Waiting for firmware update job #{job_id} to complete..."
326
-
327
- start_time = Time.now
328
- loop do
329
- status = get_job_status(job_id)
330
-
331
- case status
332
- when 'Completed'
333
- puts "Firmware update completed successfully"
334
- return true
335
- when 'Failed'
336
- raise Error, "Firmware update job failed"
337
- when 'Scheduled', 'Running', 'Downloading', 'Pending'
338
- # Job still in progress
339
- else
340
- puts "Unknown job status: #{status}"
341
- end
342
-
343
- if Time.now - start_time > timeout
344
- raise Error, "Firmware update timed out after #{timeout} seconds"
345
- end
346
-
347
- # Wait before checking again
348
- sleep 10
349
- end
350
- end
351
-
352
- def get_job_status(job_id)
353
- response = client.authenticated_request(
354
- :get,
355
- "/redfish/v1/TaskService/Tasks/#{job_id}"
356
- )
357
-
358
- if response.status != 200
359
- raise Error, "Failed to get job status with status #{response.status}: #{response.body}"
360
- end
361
-
362
- response_data = JSON.parse(response.body)
363
- response_data['TaskState'] || 'Unknown'
364
- end
365
- end
366
- end
@@ -1,49 +0,0 @@
1
- require 'httparty'
2
- require 'nokogiri'
3
-
4
- module IDRAC
5
- # Reverse engineered screenshot functionality for iDRAC
6
- # This uses introspection on how the web UI creates screenshots rather than the Redfish API
7
- class Screenshot
8
- attr_reader :client
9
-
10
- def initialize(client)
11
- @client = client
12
- end
13
-
14
- def capture
15
- # Login to get the forward URL and cookies
16
- forward_url = client.login
17
-
18
- # Extract the key-value pairs from the forward URL (format: index?ST1=ABC,ST2=DEF)
19
- tokens = forward_url.split("?").last.split(",").inject({}) do |acc, kv|
20
- k, v = kv.split("=")
21
- acc[k] = v
22
- acc
23
- end
24
-
25
- # Generate a timestamp for the request
26
- timestamp_ms = (Time.now.to_f * 1000).to_i
27
-
28
- # First request to trigger the screenshot capture
29
- path = "data?get=consolepreview[manual%20#{timestamp_ms}]"
30
- res = client.get(path: path, headers: tokens)
31
- raise Error, "Failed to trigger screenshot capture." unless res.code.between?(200, 299)
32
-
33
- # Wait for the screenshot to be generated
34
- sleep 2
35
-
36
- # Second request to get the actual screenshot image
37
- path = "capconsole/scapture0.png?#{timestamp_ms}"
38
- res = client.get(path: path, headers: tokens)
39
- raise Error, "Failed to retrieve screenshot image." unless res.code.between?(200, 299)
40
-
41
- # Save the screenshot to a file
42
- filename = "idrac_screenshot_#{timestamp_ms}.png"
43
- File.open(filename, "wb") { |f| f.write(res.body) }
44
-
45
- # Return the filename
46
- filename
47
- end
48
- end
49
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module IDRAC
4
- VERSION = "0.1.7"
5
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'httparty'
4
- require 'nokogiri'
5
- require 'faraday'
6
- require 'faraday/multipart'
7
- require 'base64'
8
- require 'uri'
9
- # If dev, required debug
10
- require 'debug' if ENV['RUBY_ENV'] == 'development'
11
-
12
- require_relative "idrac/version"
13
- require_relative "idrac/client"
14
- require_relative "idrac/screenshot"
15
- require_relative "idrac/firmware"
16
-
17
- module IDRAC
18
- class Error < StandardError; end
19
-
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
- )
29
- end
30
- end
@@ -1,4 +0,0 @@
1
- module Idrac
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end