idrac 0.7.9 → 0.8.0
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 +28 -0
- data/bin/idrac +47 -0
- data/bin/idrac-tsr +90 -0
- data/lib/idrac/boot.rb +2 -2
- data/lib/idrac/client.rb +6 -1
- data/lib/idrac/lifecycle.rb +10 -7
- data/lib/idrac/system_config.rb +51 -1
- data/lib/idrac/utility.rb +348 -0
- data/lib/idrac/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58f8d5e0d7f90857359eaf0be71c114da11ac324539b335e1a55f21567c33020
|
4
|
+
data.tar.gz: 498235abd52da11fba45a89032a5e0a72a56b53b7918e60bbdacfeda718229a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83010a42db5c2f04ce5d7c49802725e54aeb68409c2aaca00318fb32bb4c6adb69b50f22f2e2b14891934fd308b248d6b8169bcb70ba8e3f0415ea5405c3438a
|
7
|
+
data.tar.gz: d9bea26cad955e27ad749018973d999e76741b34d0645f2cc7f9abc046f977ec95d23bebbb46a2e80f990bb71cc265c7b52b1ed539dee0a8463984efa66f1864
|
data/README.md
CHANGED
@@ -17,6 +17,8 @@ A Ruby client for the Dell iDRAC API. This gem provides a command-line interface
|
|
17
17
|
- Lifecycle Controller status management
|
18
18
|
- Return values as RecursiveOpenStruct objects for convenient attribute access
|
19
19
|
- Reset iDRAC functionality
|
20
|
+
- Generate and download TSR (Technical Support Report) / SupportAssist collections
|
21
|
+
- SupportAssist EULA management
|
20
22
|
|
21
23
|
## Installation
|
22
24
|
|
@@ -85,6 +87,11 @@ idrac sel:clear --host=192.168.1.100
|
|
85
87
|
|
86
88
|
# Reset iDRAC
|
87
89
|
idrac reset --host=192.168.1.100
|
90
|
+
|
91
|
+
# TSR/SupportAssist Collection Commands
|
92
|
+
idrac tsr_collect --host=192.168.1.100 # Generate and download TSR logs
|
93
|
+
idrac tsr_status --host=192.168.1.100 # Check TSR collection status
|
94
|
+
idrac tsr_accept_eula --host=192.168.1.100 # Accept SupportAssist EULA (required before first use)
|
88
95
|
```
|
89
96
|
|
90
97
|
All commands automatically handle session expiration by re-authenticating when necessary, ensuring that long-running operations like firmware updates complete successfully even if the iDRAC session times out.
|
@@ -179,6 +186,18 @@ client.clear_lifecycle!
|
|
179
186
|
# Clear System Event Logs
|
180
187
|
client.clear_system_event_logs!
|
181
188
|
|
189
|
+
# TSR/SupportAssist Collection operations
|
190
|
+
# Accept EULA (required only once)
|
191
|
+
client.accept_supportassist_eula
|
192
|
+
|
193
|
+
# Generate and download TSR logs
|
194
|
+
file_path = client.generate_and_download_tsr(output_file: "tsr.zip")
|
195
|
+
puts "TSR logs saved to: #{file_path}"
|
196
|
+
|
197
|
+
# Check TSR collection status
|
198
|
+
status = client.tsr_status
|
199
|
+
puts "Collection available: #{status[:available]}"
|
200
|
+
|
182
201
|
# Working with hash objects
|
183
202
|
# Methods return data as Ruby hashes with string keys for consistent access
|
184
203
|
|
@@ -272,6 +291,15 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
272
291
|
|
273
292
|
## Changelog
|
274
293
|
|
294
|
+
### Version 0.8.0
|
295
|
+
- **Added TSR/SupportAssist Collection Support**: Simplified commands for generating Technical Support Reports
|
296
|
+
- Generate and download SupportAssist collections with direct file download
|
297
|
+
- Automatic EULA acceptance check with clear user guidance
|
298
|
+
- Simple CLI commands: `tsr_collect`, `tsr_status`, `tsr_accept_eula`
|
299
|
+
- Job-based generation with automatic progress tracking
|
300
|
+
- Direct download approach based on Dell's official Python implementation
|
301
|
+
- Clear error messages when EULA acceptance is required
|
302
|
+
|
275
303
|
### Version 0.7.8
|
276
304
|
- **Network Redirection Support**: Added optional `host_header` parameter to Client initialization
|
277
305
|
- Enables iDRAC access through network redirection scenarios where the connection IP differs from the Host header requirement
|
data/bin/idrac
CHANGED
@@ -1660,6 +1660,53 @@ module IDRAC
|
|
1660
1660
|
end
|
1661
1661
|
end
|
1662
1662
|
|
1663
|
+
# TSR (Technical Support Report) commands
|
1664
|
+
desc "tsr_collect", "Generate and download TSR/SupportAssist collection"
|
1665
|
+
method_option :output, type: :string, desc: "Output filename"
|
1666
|
+
def tsr_collect
|
1667
|
+
with_idrac_client do |client|
|
1668
|
+
puts "Generating and downloading TSR logs...".yellow
|
1669
|
+
puts "This may take several minutes...".yellow
|
1670
|
+
|
1671
|
+
result = client.generate_and_download_tsr(output_file: options[:output])
|
1672
|
+
|
1673
|
+
if result
|
1674
|
+
puts "TSR logs saved to: #{result}".green
|
1675
|
+
puts "File size: #{File.size(result) / 1024 / 1024} MB".cyan if File.exist?(result)
|
1676
|
+
else
|
1677
|
+
puts "Failed to generate or download TSR logs".red
|
1678
|
+
end
|
1679
|
+
end
|
1680
|
+
end
|
1681
|
+
|
1682
|
+
desc "tsr_status", "Check TSR collection status"
|
1683
|
+
def tsr_status
|
1684
|
+
with_idrac_client do |client|
|
1685
|
+
status = client.tsr_status
|
1686
|
+
|
1687
|
+
puts "\nTSR Collection Status:".green.bold
|
1688
|
+
puts "Available: #{status[:available] ? 'Yes'.green : 'No'.red}"
|
1689
|
+
|
1690
|
+
if status[:collection_in_progress]
|
1691
|
+
puts "Collection in progress: Yes".yellow
|
1692
|
+
puts "Job ID: #{status[:job_id]}".cyan if status[:job_id]
|
1693
|
+
else
|
1694
|
+
puts "Collection in progress: No".green
|
1695
|
+
end
|
1696
|
+
end
|
1697
|
+
end
|
1698
|
+
|
1699
|
+
desc "tsr_accept_eula", "Accept SupportAssist EULA"
|
1700
|
+
def tsr_accept_eula
|
1701
|
+
with_idrac_client do |client|
|
1702
|
+
if client.accept_supportassist_eula
|
1703
|
+
puts "SupportAssist EULA accepted successfully".green
|
1704
|
+
else
|
1705
|
+
puts "Failed to accept SupportAssist EULA".red
|
1706
|
+
end
|
1707
|
+
end
|
1708
|
+
end
|
1709
|
+
|
1663
1710
|
private
|
1664
1711
|
|
1665
1712
|
def with_idrac_client
|
data/bin/idrac-tsr
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/idrac'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
options = {
|
7
|
+
host: ENV['IDRAC_HOST'],
|
8
|
+
username: ENV['IDRAC_USER'] || 'root',
|
9
|
+
password: ENV['IDRAC_PASSWORD'] || 'calvin',
|
10
|
+
output: nil,
|
11
|
+
data_types: [0, 1, 2], # Default: Hardware, OS, Debug
|
12
|
+
timeout: 600,
|
13
|
+
verbose: false
|
14
|
+
}
|
15
|
+
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: idrac-tsr [options]"
|
18
|
+
|
19
|
+
opts.on("-h", "--host HOST", "iDRAC host address") do |h|
|
20
|
+
options[:host] = h
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-u", "--username USER", "Username (default: root)") do |u|
|
24
|
+
options[:username] = u
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-p", "--password PASS", "Password (default: calvin)") do |p|
|
28
|
+
options[:password] = p
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-o", "--output FILE", "Output filename (default: auto-generated)") do |o|
|
32
|
+
options[:output] = o
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-d", "--data TYPES", "Data types to collect (comma-separated: 0=HW,1=OS,2=Debug,3=TTY,4=All)") do |d|
|
36
|
+
options[:data_types] = d.split(',').map(&:to_i)
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-t", "--timeout SECONDS", Integer, "Timeout in seconds (default: 600)") do |t|
|
40
|
+
options[:timeout] = t
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
44
|
+
options[:verbose] = true
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("--help", "Show this message") do
|
48
|
+
puts opts
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
end.parse!
|
52
|
+
|
53
|
+
if options[:host].nil?
|
54
|
+
puts "Error: Host is required. Use -h HOST or set IDRAC_HOST environment variable"
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
begin
|
59
|
+
client = IDRAC::Client.new(
|
60
|
+
host: options[:host],
|
61
|
+
username: options[:username],
|
62
|
+
password: options[:password],
|
63
|
+
verify_ssl: false
|
64
|
+
)
|
65
|
+
|
66
|
+
client.verbosity = options[:verbose] ? 1 : 0
|
67
|
+
|
68
|
+
puts "Connecting to #{options[:host]}..."
|
69
|
+
client.login
|
70
|
+
|
71
|
+
puts "Generating TSR logs (this may take several minutes)..."
|
72
|
+
result = client.generate_and_download_tsr(
|
73
|
+
output_file: options[:output],
|
74
|
+
data_selector_values: options[:data_types],
|
75
|
+
wait_timeout: options[:timeout]
|
76
|
+
)
|
77
|
+
|
78
|
+
if result
|
79
|
+
puts "✓ TSR logs saved to: #{result}"
|
80
|
+
puts " File size: #{File.size(result) / 1024 / 1024} MB" if File.exist?(result)
|
81
|
+
else
|
82
|
+
puts "✗ Failed to download TSR logs"
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
|
86
|
+
client.logout
|
87
|
+
rescue => e
|
88
|
+
puts "Error: #{e.message}"
|
89
|
+
exit 1
|
90
|
+
end
|
data/lib/idrac/boot.rb
CHANGED
@@ -356,7 +356,7 @@ module IDRAC
|
|
356
356
|
else
|
357
357
|
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
|
358
358
|
json = JSON.parse(response.body)
|
359
|
-
raise "Error reading HddPlaceholder setup" if json&.dig('
|
359
|
+
raise "Error reading HddPlaceholder setup" if json&.dig('Attributes','HddPlaceholder').blank?
|
360
360
|
json["Attributes"]["HddPlaceholder"] == "Enabled"
|
361
361
|
end
|
362
362
|
end
|
@@ -371,7 +371,7 @@ module IDRAC
|
|
371
371
|
else
|
372
372
|
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
|
373
373
|
json = JSON.parse(response.body)
|
374
|
-
raise "Error reading PowerControl setup" if json&.dig('
|
374
|
+
raise "Error reading PowerControl setup" if json&.dig('Attributes').blank?
|
375
375
|
json["Attributes"]["ProcCStates"] == "Enabled" &&
|
376
376
|
json["Attributes"]["SysProfile"] == "PerfPerWattOptimizedOs" &&
|
377
377
|
json["Attributes"]["ProcPwrPerf"] == "OsDbpm"
|
data/lib/idrac/client.rb
CHANGED
@@ -155,6 +155,11 @@ module IDRAC
|
|
155
155
|
headers['Accept'] ||= 'application/json'
|
156
156
|
headers['Host'] = @host_header if @host_header
|
157
157
|
|
158
|
+
# Debug the body being sent
|
159
|
+
if body && @verbosity >= 2
|
160
|
+
debug "Request body: #{body}", 2
|
161
|
+
end
|
162
|
+
|
158
163
|
# Determine authentication method and set headers
|
159
164
|
if @direct_mode
|
160
165
|
headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
@@ -191,7 +196,7 @@ module IDRAC
|
|
191
196
|
conn.options.timeout = timeout if timeout
|
192
197
|
conn.options.open_timeout = open_timeout if open_timeout
|
193
198
|
|
194
|
-
conn.run_request(method, path
|
199
|
+
conn.run_request(method, path, body, headers)
|
195
200
|
ensure
|
196
201
|
conn.options.timeout = original_timeout
|
197
202
|
conn.options.open_timeout = original_open_timeout
|
data/lib/idrac/lifecycle.rb
CHANGED
@@ -13,12 +13,13 @@ module IDRAC
|
|
13
13
|
debug "Detected iDRAC version: #{idrac_version}", 1
|
14
14
|
|
15
15
|
# Use version-specific methods
|
16
|
-
if idrac_version
|
16
|
+
if idrac_version >= 9
|
17
17
|
debug "Using modern approach for iDRAC > 9", 1
|
18
18
|
return get_lifecycle_status_modern_firmware
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
# This may have been one particularly odd oldish iDRAC 9
|
20
|
+
# elsif idrac_version == 9
|
21
|
+
# debug "Using registry approach for iDRAC 9", 1
|
22
|
+
# return get_lifecycle_status_from_registry
|
22
23
|
else
|
23
24
|
debug "Using SCP approach for older iDRAC (v#{idrac_version})", 1
|
24
25
|
return get_lifecycle_status_from_scp
|
@@ -57,7 +58,7 @@ module IDRAC
|
|
57
58
|
return false
|
58
59
|
rescue => e
|
59
60
|
debug "Error getting Lifecycle Controller status from SCP: #{e.message}", 1, :red
|
60
|
-
debug e.backtrace.join("\n"),
|
61
|
+
debug e.backtrace.join("\n"), 10, :red
|
61
62
|
return false
|
62
63
|
end
|
63
64
|
end
|
@@ -75,9 +76,11 @@ module IDRAC
|
|
75
76
|
# This is the attribute we want:
|
76
77
|
target = attributes&.dig('RegistryEntries', 'Attributes')&.find {|q| q['AttributeName'] =~ /LCAttributes.1.LifecycleControllerState/ }
|
77
78
|
# This is the FQDN of the attribute we want to get the value of:
|
78
|
-
fqdn = target.dig('Id')
|
79
|
+
fqdn = target.dig('Id') # LifecycleController.Embedded.1#LCAttributes.1#LifecycleControllerState
|
80
|
+
subpath = fqdn.gsub(/#.*$/,'') # Remove everything # and onwards
|
79
81
|
# This is the Current Value:
|
80
|
-
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellAttributes/#{
|
82
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellAttributes/#{subpath}")
|
83
|
+
|
81
84
|
if response.status != 200
|
82
85
|
debug "Failed to get Lifecycle Controller Attributes".red, 1
|
83
86
|
return false
|
data/lib/idrac/system_config.rb
CHANGED
@@ -193,7 +193,36 @@ module IDRAC
|
|
193
193
|
end
|
194
194
|
end
|
195
195
|
|
196
|
-
|
196
|
+
# Merge multiple SCP configurations together
|
197
|
+
# Takes multiple arguments - each can be an SCP hash, array of components, or full SCP structure
|
198
|
+
def merge_scp(*scps)
|
199
|
+
merged_components = {}
|
200
|
+
|
201
|
+
scps.compact.each do |scp|
|
202
|
+
components = extract_components(scp)
|
203
|
+
components.each do |component|
|
204
|
+
fqdd = component["FQDD"]
|
205
|
+
if merged_components[fqdd]
|
206
|
+
# Merge attributes for the same FQDD
|
207
|
+
existing_attrs = merged_components[fqdd]["Attributes"] || []
|
208
|
+
new_attrs = component["Attributes"] || []
|
209
|
+
|
210
|
+
# Build hash of existing attributes by name for easy lookup
|
211
|
+
attr_hash = {}
|
212
|
+
existing_attrs.each { |attr| attr_hash[attr["Name"]] = attr }
|
213
|
+
|
214
|
+
# Add/overwrite with new attributes
|
215
|
+
new_attrs.each { |attr| attr_hash[attr["Name"]] = attr }
|
216
|
+
|
217
|
+
merged_components[fqdd]["Attributes"] = attr_hash.values
|
218
|
+
else
|
219
|
+
merged_components[fqdd] = component.dup
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
merged_components.values
|
225
|
+
end
|
197
226
|
|
198
227
|
# Handle location header for IP change operations. Monitors old IP until it fails,
|
199
228
|
# then aggressively monitors new IP with tight timeouts.
|
@@ -266,5 +295,26 @@ module IDRAC
|
|
266
295
|
end
|
267
296
|
|
268
297
|
private
|
298
|
+
|
299
|
+
# Extract components array from various SCP formats
|
300
|
+
def extract_components(scp)
|
301
|
+
case scp
|
302
|
+
when Hash
|
303
|
+
if scp["SystemConfiguration"] && scp["SystemConfiguration"]["Components"]
|
304
|
+
scp["SystemConfiguration"]["Components"]
|
305
|
+
elsif scp["Components"]
|
306
|
+
scp["Components"]
|
307
|
+
elsif scp["FQDD"]
|
308
|
+
[scp] # Single component
|
309
|
+
else
|
310
|
+
# Assume it's a hash of FQDD => attributes
|
311
|
+
hash_to_scp(scp)
|
312
|
+
end
|
313
|
+
when Array
|
314
|
+
scp # Already an array of components
|
315
|
+
else
|
316
|
+
[]
|
317
|
+
end
|
318
|
+
end
|
269
319
|
end
|
270
320
|
end
|
data/lib/idrac/utility.rb
CHANGED
@@ -1,7 +1,355 @@
|
|
1
|
+
|
1
2
|
module IDRAC
|
2
3
|
module Utility
|
3
4
|
include Debuggable
|
4
5
|
|
6
|
+
# Generate TSR (Technical Support Report) logs using SupportAssistCollection for local generation
|
7
|
+
# @param data_selector_values [Array] Array of log types to include (optional)
|
8
|
+
# Default includes all available log types
|
9
|
+
# @return [Hash] Result hash with status and job/task information
|
10
|
+
def generate_tsr_logs(data_selector_values: nil, share_type: nil, share_parameters: nil)
|
11
|
+
debug "Generating TSR/SupportAssist logs...", 1
|
12
|
+
|
13
|
+
# Check EULA status first
|
14
|
+
eula_status = supportassist_eula_status
|
15
|
+
if eula_status["EULAAccepted"] == false || eula_status["EULAAccepted"] == "false"
|
16
|
+
puts "\n" + "="*80
|
17
|
+
puts "ERROR: SupportAssist EULA Not Accepted".red.bold
|
18
|
+
puts "="*80
|
19
|
+
puts ""
|
20
|
+
puts "The SupportAssist End User License Agreement (EULA) must be accepted".yellow
|
21
|
+
puts "before you can generate TSR/SupportAssist collections.".yellow
|
22
|
+
puts ""
|
23
|
+
puts "To accept the EULA, run:".cyan
|
24
|
+
puts " idrac tsr_accept_eula --host #{@host} --port #{@port}".green.bold
|
25
|
+
puts ""
|
26
|
+
puts "="*80
|
27
|
+
return { status: :failed, error: "SupportAssist EULA not accepted" }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Default data selector values for comprehensive TSR
|
31
|
+
# Valid values for SupportAssistCollection: "DebugLogs", "GPULogs", "HWData", "OSAppData", "TTYLogs", "TelemetryReports"
|
32
|
+
data_selector_values ||= ["HWData", "OSAppData"]
|
33
|
+
|
34
|
+
# Map numeric values to iDRAC expected strings if needed
|
35
|
+
if data_selector_values.is_a?(Array) && data_selector_values.first.to_s =~ /^\d+$/
|
36
|
+
data_selector_values = data_selector_values.map do |val|
|
37
|
+
case val.to_s
|
38
|
+
when "0" then "HWData"
|
39
|
+
when "1" then "OSAppData"
|
40
|
+
when "2" then "TTYLogs"
|
41
|
+
when "3" then "DebugLogs"
|
42
|
+
else "HWData" # Default to HWData
|
43
|
+
end
|
44
|
+
end
|
45
|
+
elsif data_selector_values.is_a?(String)
|
46
|
+
data_selector_values = data_selector_values.split(',')
|
47
|
+
end
|
48
|
+
|
49
|
+
debug "Data selector values: #{data_selector_values.inspect}", 1
|
50
|
+
|
51
|
+
# Use SupportAssistCollection for local generation as it supports "Local" ShareType
|
52
|
+
payload = {
|
53
|
+
"ShareType" => "Local",
|
54
|
+
"DataSelectorArrayIn" => data_selector_values,
|
55
|
+
"Filter" => "No", # Don't filter PII
|
56
|
+
"Transmit" => "No" # Don't transmit to Dell
|
57
|
+
}
|
58
|
+
|
59
|
+
debug "SupportAssist collection payload: #{payload.to_json}", 1
|
60
|
+
|
61
|
+
response = authenticated_request(
|
62
|
+
:post,
|
63
|
+
"/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.SupportAssistCollection",
|
64
|
+
body: payload.to_json,
|
65
|
+
headers: { 'Content-Type' => 'application/json' }
|
66
|
+
)
|
67
|
+
|
68
|
+
case response.status
|
69
|
+
when 202
|
70
|
+
# Accepted - job created
|
71
|
+
location = response.headers["location"]
|
72
|
+
if location
|
73
|
+
debug "TSR generation job created: #{location}", 1, :green
|
74
|
+
job_id = location.split("/").last
|
75
|
+
# Wait for job to complete and capture the file location
|
76
|
+
job_result = wait_for_job_with_location(job_id)
|
77
|
+
if job_result && (job_result["JobState"] == "Completed" || job_result["JobState"] == "CompletedWithErrors")
|
78
|
+
result = { status: :success, job: job_result }
|
79
|
+
# Check if we got a file location from the job completion
|
80
|
+
result[:location] = job_result["FileLocation"] if job_result["FileLocation"]
|
81
|
+
result
|
82
|
+
else
|
83
|
+
{ status: :failed, error: "Job did not complete successfully" }
|
84
|
+
end
|
85
|
+
else
|
86
|
+
{ status: :accepted, message: "TSR generation initiated" }
|
87
|
+
end
|
88
|
+
when 200..299
|
89
|
+
debug "TSR generation completed immediately", 1, :green
|
90
|
+
{ status: :success }
|
91
|
+
else
|
92
|
+
error_msg = parse_error_response(response)
|
93
|
+
debug "Failed to generate TSR: #{error_msg}", 1, :red
|
94
|
+
{ status: :failed, error: error_msg }
|
95
|
+
end
|
96
|
+
rescue => e
|
97
|
+
debug "Error generating TSR: #{e.message}", 1, :red
|
98
|
+
{ status: :error, error: e.message }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Download TSR/SupportAssist logs from a URL location
|
102
|
+
# @param location [String] URL location of the TSR file
|
103
|
+
# @param output_file [String] Path to save the TSR file (optional)
|
104
|
+
# @return [String, nil] Path to downloaded file or nil if failed
|
105
|
+
def download_tsr_from_location(location, output_file: nil)
|
106
|
+
debug "Downloading TSR from location: #{location}", 1
|
107
|
+
|
108
|
+
# Default output filename with timestamp
|
109
|
+
output_file ||= "supportassist_#{@host}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.zip"
|
110
|
+
|
111
|
+
# Download the file from the location
|
112
|
+
file_response = authenticated_request(:get, location)
|
113
|
+
|
114
|
+
if file_response.status == 200 && file_response.body
|
115
|
+
File.open(output_file, 'wb') do |f|
|
116
|
+
f.write(file_response.body)
|
117
|
+
end
|
118
|
+
debug "TSR saved to: #{output_file} (#{File.size(output_file)} bytes)", 1, :green
|
119
|
+
return output_file
|
120
|
+
else
|
121
|
+
debug "Failed to download file from location. Status: #{file_response.status}", 1, :red
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
rescue => e
|
125
|
+
debug "Error downloading TSR: #{e.message}", 1, :red
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
|
129
|
+
# Wait for job and capture file location from response headers
|
130
|
+
# @param job_id [String] The job ID to wait for
|
131
|
+
# @param max_wait [Integer] Maximum time to wait in seconds
|
132
|
+
# @return [Hash, nil] Job data with FileLocation if available
|
133
|
+
def wait_for_job_with_location(job_id, max_wait: 600)
|
134
|
+
debug "Waiting for job #{job_id} to complete...", 1
|
135
|
+
start_time = Time.now
|
136
|
+
|
137
|
+
while (Time.now - start_time) < max_wait
|
138
|
+
job_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{job_id}")
|
139
|
+
|
140
|
+
if job_response.status == 200
|
141
|
+
job_data = JSON.parse(job_response.body)
|
142
|
+
|
143
|
+
case job_data["JobState"]
|
144
|
+
when "Completed", "CompletedWithErrors"
|
145
|
+
debug "Job #{job_id} completed: #{job_data["JobState"]}", 1, :green
|
146
|
+
|
147
|
+
# Check response headers for file location
|
148
|
+
if job_response.headers["location"]
|
149
|
+
job_data["FileLocation"] = job_response.headers["location"]
|
150
|
+
debug "Found file location in headers: #{job_data["FileLocation"]}", 1, :green
|
151
|
+
end
|
152
|
+
|
153
|
+
# Also check the job data itself for output location
|
154
|
+
if job_data["Oem"] && job_data["Oem"]["Dell"] && job_data["Oem"]["Dell"]["OutputLocation"]
|
155
|
+
job_data["FileLocation"] = job_data["Oem"]["Dell"]["OutputLocation"]
|
156
|
+
debug "Found file location in job data: #{job_data["FileLocation"]}", 1, :green
|
157
|
+
end
|
158
|
+
|
159
|
+
return job_data
|
160
|
+
when "Failed", "Exception"
|
161
|
+
debug "Job #{job_id} failed: #{job_data["Message"]}", 1, :red
|
162
|
+
return job_data
|
163
|
+
else
|
164
|
+
debug "Job #{job_id} state: #{job_data["JobState"]} - #{job_data["PercentComplete"]}%", 2
|
165
|
+
sleep 5
|
166
|
+
end
|
167
|
+
else
|
168
|
+
debug "Failed to get job status: #{job_response.status}", 2
|
169
|
+
sleep 5
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
debug "Timeout waiting for job #{job_id}", 1, :red
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
177
|
+
# Parse error response from iDRAC
|
178
|
+
def parse_error_response(response)
|
179
|
+
begin
|
180
|
+
data = JSON.parse(response.body)
|
181
|
+
if data["error"] && data["error"]["@Message.ExtendedInfo"]
|
182
|
+
data["error"]["@Message.ExtendedInfo"].first["Message"]
|
183
|
+
elsif data["error"] && data["error"]["message"]
|
184
|
+
data["error"]["message"]
|
185
|
+
else
|
186
|
+
"Status: #{response.status} - #{response.body}"
|
187
|
+
end
|
188
|
+
rescue
|
189
|
+
"Status: #{response.status} - #{response.body}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Generate and download TSR logs in a single operation
|
194
|
+
# @param output_file [String] Path to save the TSR file (optional)
|
195
|
+
# @param data_selector_values [Array] Array of log types to include (optional)
|
196
|
+
# @param wait_timeout [Integer] Maximum time to wait for generation in seconds (default: 600)
|
197
|
+
# @return [String, nil] Path to downloaded file or nil if failed
|
198
|
+
def generate_and_download_tsr(output_file: nil, data_selector_values: nil, wait_timeout: 600)
|
199
|
+
debug "Starting TSR generation and download process...", 1
|
200
|
+
|
201
|
+
output_file ||= "supportassist_#{@host}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.zip"
|
202
|
+
|
203
|
+
# First, generate the TSR
|
204
|
+
result = generate_tsr_logs(data_selector_values: data_selector_values)
|
205
|
+
|
206
|
+
if result[:status] == :success && result[:job]
|
207
|
+
debug "TSR generation completed successfully", 1, :green
|
208
|
+
|
209
|
+
# Check if the job response has a location for the file
|
210
|
+
if result[:location]
|
211
|
+
return download_tsr_from_location(result[:location], output_file: output_file)
|
212
|
+
else
|
213
|
+
# Try alternative download methods based on Dell's Python script approach
|
214
|
+
debug "Attempting to locate generated TSR file...", 1, :yellow
|
215
|
+
|
216
|
+
# Wait a moment for the file to be available
|
217
|
+
sleep 2
|
218
|
+
|
219
|
+
# Try known endpoints where the file might be available
|
220
|
+
possible_locations = [
|
221
|
+
"/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/ExportedFiles/SupportAssist",
|
222
|
+
"/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLCService/ExportedFiles",
|
223
|
+
"/downloads/supportassist_collection.zip",
|
224
|
+
"/sysmgmt/2016/server/support_assist_collection"
|
225
|
+
]
|
226
|
+
|
227
|
+
possible_locations.each do |location|
|
228
|
+
debug "Trying location: #{location}", 2
|
229
|
+
file_response = authenticated_request(:get, location)
|
230
|
+
|
231
|
+
if file_response.status == 200 && file_response.body && file_response.body.size > 1024
|
232
|
+
File.open(output_file, 'wb') do |f|
|
233
|
+
f.write(file_response.body)
|
234
|
+
end
|
235
|
+
debug "TSR saved to: #{output_file} (#{File.size(output_file)} bytes)", 1, :green
|
236
|
+
return output_file
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
debug "Could not locate TSR file for direct download", 1, :yellow
|
241
|
+
debug "The collection was generated but may require network share export", 1, :yellow
|
242
|
+
end
|
243
|
+
elsif result[:status] == :accepted
|
244
|
+
debug "TSR generation was accepted but status unknown", 1, :yellow
|
245
|
+
else
|
246
|
+
debug "Failed to initiate TSR generation: #{result[:error]}", 1, :red
|
247
|
+
end
|
248
|
+
|
249
|
+
nil
|
250
|
+
end
|
251
|
+
public
|
252
|
+
|
253
|
+
# Get TSR/SupportAssist collection status
|
254
|
+
# @return [Hash] Status information
|
255
|
+
def tsr_status
|
256
|
+
debug "Checking SupportAssist collection status...", 1
|
257
|
+
|
258
|
+
response = authenticated_request(
|
259
|
+
:get,
|
260
|
+
"/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService"
|
261
|
+
)
|
262
|
+
|
263
|
+
if response.status == 200
|
264
|
+
data = JSON.parse(response.body)
|
265
|
+
status = {
|
266
|
+
available: data["Actions"]&.key?("#DellLCService.SupportAssistCollection"),
|
267
|
+
export_available: data["Actions"]&.key?("#DellLCService.SupportAssistExportLastCollection"),
|
268
|
+
collection_in_progress: false
|
269
|
+
}
|
270
|
+
|
271
|
+
# Check if there's an active collection job
|
272
|
+
jobs_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs")
|
273
|
+
if jobs_response.status == 200
|
274
|
+
jobs_data = JSON.parse(jobs_response.body)
|
275
|
+
if jobs_data["Members"]
|
276
|
+
jobs_data["Members"].each do |job|
|
277
|
+
if job["Name"]&.include?("SupportAssist") || job["Name"]&.include?("TSR")
|
278
|
+
status[:collection_in_progress] = true
|
279
|
+
status[:job_id] = job["Id"]
|
280
|
+
status[:job_state] = job["JobState"]
|
281
|
+
break
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
debug "SupportAssist status: #{status.to_json}", 2
|
288
|
+
status
|
289
|
+
else
|
290
|
+
debug "Failed to get SupportAssist status: #{response.status}", 1, :red
|
291
|
+
{ available: false, error: "Unable to determine status" }
|
292
|
+
end
|
293
|
+
rescue => e
|
294
|
+
debug "Error checking SupportAssist status: #{e.message}", 1, :red
|
295
|
+
{ available: false, error: e.message }
|
296
|
+
end
|
297
|
+
|
298
|
+
# Check SupportAssist EULA status
|
299
|
+
# @return [Hash] EULA status information
|
300
|
+
def supportassist_eula_status
|
301
|
+
debug "Checking SupportAssist EULA status...", 1
|
302
|
+
|
303
|
+
response = authenticated_request(
|
304
|
+
:post,
|
305
|
+
"/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.SupportAssistGetEULAStatus",
|
306
|
+
body: {}.to_json,
|
307
|
+
headers: { 'Content-Type' => 'application/json' }
|
308
|
+
)
|
309
|
+
|
310
|
+
if response.status.between?(200, 299)
|
311
|
+
begin
|
312
|
+
data = JSON.parse(response.body)
|
313
|
+
debug "EULA status: #{data.to_json}", 2
|
314
|
+
return data
|
315
|
+
rescue JSON::ParserError
|
316
|
+
return { "EULAAccepted" => "Unknown" }
|
317
|
+
end
|
318
|
+
else
|
319
|
+
error_msg = parse_error_response(response)
|
320
|
+
debug "Failed to get EULA status: #{error_msg}", 1, :red
|
321
|
+
return { "EULAAccepted" => "Error", "error" => error_msg }
|
322
|
+
end
|
323
|
+
rescue => e
|
324
|
+
debug "Error checking EULA status: #{e.message}", 1, :red
|
325
|
+
{ "EULAAccepted" => "Error", "error" => e.message }
|
326
|
+
end
|
327
|
+
|
328
|
+
# Accept SupportAssist EULA
|
329
|
+
# @return [Boolean] true if successful
|
330
|
+
def accept_supportassist_eula
|
331
|
+
debug "Accepting SupportAssist EULA...", 1
|
332
|
+
|
333
|
+
response = authenticated_request(
|
334
|
+
:post,
|
335
|
+
"/redfish/v1/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.SupportAssistAcceptEULA",
|
336
|
+
body: {}.to_json,
|
337
|
+
headers: { 'Content-Type' => 'application/json' }
|
338
|
+
)
|
339
|
+
|
340
|
+
if response.status.between?(200, 299)
|
341
|
+
debug "SupportAssist EULA accepted successfully", 1, :green
|
342
|
+
true
|
343
|
+
else
|
344
|
+
error_msg = parse_error_response(response)
|
345
|
+
debug "Failed to accept EULA: #{error_msg}", 1, :red
|
346
|
+
false
|
347
|
+
end
|
348
|
+
rescue => e
|
349
|
+
debug "Error accepting EULA: #{e.message}", 1, :red
|
350
|
+
false
|
351
|
+
end
|
352
|
+
|
5
353
|
# Reset the iDRAC controller (graceful restart)
|
6
354
|
def reset!
|
7
355
|
debug "Resetting iDRAC controller...", 1
|
data/lib/idrac/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: idrac
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: httparty
|
@@ -261,6 +260,7 @@ files:
|
|
261
260
|
- README.md
|
262
261
|
- bin/console
|
263
262
|
- bin/idrac
|
263
|
+
- bin/idrac-tsr
|
264
264
|
- bin/setup
|
265
265
|
- idrac.gemspec
|
266
266
|
- lib/idrac.rb
|
@@ -286,7 +286,6 @@ licenses:
|
|
286
286
|
- MIT
|
287
287
|
metadata:
|
288
288
|
homepage_uri: http://github.com
|
289
|
-
post_install_message:
|
290
289
|
rdoc_options: []
|
291
290
|
require_paths:
|
292
291
|
- lib
|
@@ -301,8 +300,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
301
300
|
- !ruby/object:Gem::Version
|
302
301
|
version: '0'
|
303
302
|
requirements: []
|
304
|
-
rubygems_version: 3.
|
305
|
-
signing_key:
|
303
|
+
rubygems_version: 3.6.7
|
306
304
|
specification_version: 4
|
307
305
|
summary: API Client for Dell iDRAC
|
308
306
|
test_files: []
|