idrac 0.8.2 → 0.8.4
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/bin/idrac-tsr +90 -0
- data/lib/idrac/boot.rb +224 -1
- data/lib/idrac/client.rb +1 -0
- data/lib/idrac/jobs.rb +20 -23
- data/lib/idrac/network.rb +126 -0
- data/lib/idrac/storage.rb +77 -8
- data/lib/idrac/system.rb +56 -7
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac/virtual_media.rb +0 -27
- data/lib/idrac.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f2dd002b1876905499180d33abb476ed732f945aef7df82ed55bc68887763fd
|
4
|
+
data.tar.gz: a6a90ac44186a45ca043af595a2797d992d998b9da1a75a2d569c1ea2c7f0c1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a75656b84dcc40e9fb80b65787dde092b71391cdb7d680c847cadd77394dc00c4081a772ed742d4ad758a4e075e1a00ef4a01ef2571a8901fa1347b176e1d578
|
7
|
+
data.tar.gz: ac5962c3858f449c433cc9b76ad61bc72f316af028796ab04f363ce0f3cfd786ff3903fd7e09e07f220d5a7d40d45c4d591246eabcc31648a4071a2d27245241
|
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
@@ -28,8 +28,229 @@ require 'colorize'
|
|
28
28
|
# https://github.com/dell/iDRAC-Redfish-Scripting/issues/116
|
29
29
|
module IDRAC
|
30
30
|
module Boot
|
31
|
-
# Get
|
31
|
+
# Get boot configuration with snake_case fields
|
32
|
+
def boot_config
|
33
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
34
|
+
|
35
|
+
if response.status == 200
|
36
|
+
begin
|
37
|
+
data = JSON.parse(response.body)
|
38
|
+
boot_data = data["Boot"] || {}
|
39
|
+
|
40
|
+
# Get boot options for resolving references
|
41
|
+
options_map = {}
|
42
|
+
begin
|
43
|
+
options = boot_options
|
44
|
+
options.each do |opt|
|
45
|
+
options_map[opt["id"]] = opt["display_name"] || opt["name"]
|
46
|
+
end
|
47
|
+
rescue
|
48
|
+
# Ignore errors fetching boot options
|
49
|
+
end
|
50
|
+
|
51
|
+
# Build boot order with resolved names
|
52
|
+
boot_order = (boot_data["BootOrder"] || []).map do |ref|
|
53
|
+
{
|
54
|
+
"reference" => ref,
|
55
|
+
"name" => options_map[ref] || ref
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return hash with snake_case fields
|
60
|
+
{
|
61
|
+
# Boot override settings (for one-time or continuous boot)
|
62
|
+
"boot_source_override_enabled" => boot_data["BootSourceOverrideEnabled"], # Disabled/Once/Continuous
|
63
|
+
"boot_source_override_target" => boot_data["BootSourceOverrideTarget"], # None/Pxe/Hdd/Cd/etc
|
64
|
+
"boot_source_override_mode" => boot_data["BootSourceOverrideMode"], # UEFI/Legacy
|
65
|
+
"allowed_override_targets" => boot_data["BootSourceOverrideTarget@Redfish.AllowableValues"] || [],
|
66
|
+
|
67
|
+
# Permanent boot order with resolved names
|
68
|
+
"boot_order" => boot_order, # [{reference: "Boot0001", name: "Ubuntu"}]
|
69
|
+
"boot_order_refs" => boot_data["BootOrder"] || [], # Raw references for set_boot_order
|
70
|
+
|
71
|
+
# UEFI specific fields
|
72
|
+
"uefi_target_boot_source_override" => boot_data["UefiTargetBootSourceOverride"],
|
73
|
+
"stop_boot_on_fault" => boot_data["StopBootOnFault"],
|
74
|
+
|
75
|
+
# References to other resources
|
76
|
+
"boot_options_uri" => boot_data.dig("BootOptions", "@odata.id"),
|
77
|
+
"certificates_uri" => boot_data.dig("Certificates", "@odata.id")
|
78
|
+
}.compact
|
79
|
+
rescue JSON::ParserError
|
80
|
+
raise Error, "Failed to parse boot response: #{response.body}"
|
81
|
+
end
|
82
|
+
else
|
83
|
+
raise Error, "Failed to get boot configuration. Status code: #{response.status}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get raw Redfish boot data (CamelCase)
|
88
|
+
def boot_raw
|
89
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
90
|
+
|
91
|
+
if response.status == 200
|
92
|
+
data = JSON.parse(response.body)
|
93
|
+
data["Boot"] || {}
|
94
|
+
else
|
95
|
+
raise Error, "Failed to get boot configuration. Status code: #{response.status}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Shorter alias for convenience
|
100
|
+
def boot
|
101
|
+
boot_config
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get boot options collection - the actual boot devices present in the system
|
105
|
+
# This is different from boot_config which returns the boot configuration settings
|
106
|
+
def boot_options
|
107
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootOptions?$expand=*($levels=1)")
|
108
|
+
|
109
|
+
if response.status == 200
|
110
|
+
begin
|
111
|
+
data = JSON.parse(response.body)
|
112
|
+
|
113
|
+
# Return the BootOption objects with snake_case
|
114
|
+
data["Members"]&.map do |member|
|
115
|
+
{
|
116
|
+
"id" => member["Id"], # Boot0001
|
117
|
+
"boot_option_reference" => member["BootOptionReference"], # Boot0001
|
118
|
+
"display_name" => member["DisplayName"], # "Integrated RAID Controller 1: Ubuntu"
|
119
|
+
"name" => member["DisplayName"] || member["Name"], # Alias for display_name
|
120
|
+
"enabled" => member["BootOptionEnabled"], # true/false
|
121
|
+
"uefi_device_path" => member["UefiDevicePath"], # UEFI device path
|
122
|
+
"description" => member["Description"]
|
123
|
+
}.compact
|
124
|
+
end || []
|
125
|
+
rescue JSON::ParserError
|
126
|
+
raise Error, "Failed to parse boot options response: #{response.body}"
|
127
|
+
end
|
128
|
+
else
|
129
|
+
[]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Legacy method names for backward compatibility
|
32
134
|
def get_bios_boot_options
|
135
|
+
get_bios_boot_sources
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_boot_devices
|
139
|
+
boot_options
|
140
|
+
end
|
141
|
+
|
142
|
+
# Set boot override for next boot
|
143
|
+
def set_boot_override(target, enabled: "Once", mode: nil)
|
144
|
+
# Validate target against allowed values
|
145
|
+
boot_data = boot
|
146
|
+
valid_targets = boot_data["allowed_override_targets"]
|
147
|
+
|
148
|
+
if valid_targets && !valid_targets.include?(target)
|
149
|
+
debug "Invalid boot target '#{target}'. Allowed values: #{valid_targets.join(', ')}", 1, :red
|
150
|
+
raise Error, "Invalid boot target: #{target}"
|
151
|
+
end
|
152
|
+
|
153
|
+
debug "Setting boot override to #{target} (#{enabled})...", 1, :yellow
|
154
|
+
|
155
|
+
body = {
|
156
|
+
"Boot" => {
|
157
|
+
"BootSourceOverrideEnabled" => enabled, # Disabled/Once/Continuous
|
158
|
+
"BootSourceOverrideTarget" => target # None/Pxe/Hdd/Cd/etc
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
# Add boot mode if specified
|
163
|
+
body["Boot"]["BootSourceOverrideMode"] = mode if mode
|
164
|
+
|
165
|
+
response = authenticated_request(
|
166
|
+
:patch,
|
167
|
+
"/redfish/v1/Systems/System.Embedded.1",
|
168
|
+
body: body.to_json,
|
169
|
+
headers: { 'Content-Type': 'application/json' }
|
170
|
+
)
|
171
|
+
|
172
|
+
if response.status.between?(200, 299)
|
173
|
+
debug "Boot override set successfully.", 1, :green
|
174
|
+
return true
|
175
|
+
else
|
176
|
+
raise Error, "Failed to set boot override: #{response.status} - #{response.body}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Clear boot override settings
|
181
|
+
def clear_boot_override
|
182
|
+
debug "Clearing boot override...", 1, :yellow
|
183
|
+
|
184
|
+
body = {
|
185
|
+
"Boot" => {
|
186
|
+
"BootSourceOverrideEnabled" => "Disabled"
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
response = authenticated_request(
|
191
|
+
:patch,
|
192
|
+
"/redfish/v1/Systems/System.Embedded.1",
|
193
|
+
body: body.to_json,
|
194
|
+
headers: { 'Content-Type': 'application/json' }
|
195
|
+
)
|
196
|
+
|
197
|
+
if response.status.between?(200, 299)
|
198
|
+
debug "Boot override cleared successfully.", 1, :green
|
199
|
+
return true
|
200
|
+
else
|
201
|
+
raise Error, "Failed to clear boot override: #{response.status} - #{response.body}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Set the permanent boot order
|
206
|
+
def set_boot_order(devices)
|
207
|
+
debug "Setting boot order...", 1, :yellow
|
208
|
+
|
209
|
+
body = {
|
210
|
+
"Boot" => {
|
211
|
+
"BootOrder" => devices
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
response = authenticated_request(
|
216
|
+
:patch,
|
217
|
+
"/redfish/v1/Systems/System.Embedded.1",
|
218
|
+
body: body.to_json,
|
219
|
+
headers: { 'Content-Type': 'application/json' }
|
220
|
+
)
|
221
|
+
|
222
|
+
if response.status.between?(200, 299)
|
223
|
+
debug "Boot order set successfully.", 1, :green
|
224
|
+
return true
|
225
|
+
else
|
226
|
+
raise Error, "Failed to set boot order: #{response.status} - #{response.body}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Convenience methods for common boot targets
|
231
|
+
def boot_to_pxe(enabled: "Once", mode: nil)
|
232
|
+
set_boot_override("Pxe", enabled: enabled, mode: mode)
|
233
|
+
end
|
234
|
+
|
235
|
+
def boot_to_disk(enabled: "Once", mode: nil)
|
236
|
+
set_boot_override("Hdd", enabled: enabled, mode: mode)
|
237
|
+
end
|
238
|
+
|
239
|
+
def boot_to_cd(enabled: "Once", mode: nil)
|
240
|
+
set_boot_override("Cd", enabled: enabled, mode: mode)
|
241
|
+
end
|
242
|
+
|
243
|
+
def boot_to_usb(enabled: "Once", mode: nil)
|
244
|
+
set_boot_override("Usb", enabled: enabled, mode: mode)
|
245
|
+
end
|
246
|
+
|
247
|
+
def boot_to_bios_setup(enabled: "Once", mode: nil)
|
248
|
+
set_boot_override("BiosSetup", enabled: enabled, mode: mode)
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
def get_bios_boot_sources
|
33
254
|
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootSources")
|
34
255
|
|
35
256
|
if response.status == 200
|
@@ -62,6 +283,8 @@ module IDRAC
|
|
62
283
|
end
|
63
284
|
end
|
64
285
|
|
286
|
+
public
|
287
|
+
|
65
288
|
# Ensure UEFI boot mode
|
66
289
|
def ensure_uefi_boot
|
67
290
|
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
|
data/lib/idrac/client.rb
CHANGED
@@ -23,6 +23,7 @@ module IDRAC
|
|
23
23
|
include License
|
24
24
|
include SystemConfig
|
25
25
|
include Utility
|
26
|
+
include Network
|
26
27
|
|
27
28
|
def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, retry_count: 3, retry_delay: 1, host_header: nil)
|
28
29
|
@host = host
|
data/lib/idrac/jobs.rb
CHANGED
@@ -42,33 +42,30 @@ module IDRAC
|
|
42
42
|
|
43
43
|
# Clear all jobs from the job queue
|
44
44
|
def clear_jobs!
|
45
|
-
# Get list of jobs
|
46
45
|
jobs_response = authenticated_request(:get, '/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)')
|
47
|
-
|
46
|
+
return true unless jobs_response.status == 200
|
48
47
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
puts "
|
65
|
-
return true
|
66
|
-
rescue JSON::ParserError
|
67
|
-
raise Error, "Failed to parse jobs response: #{jobs_response.body}"
|
48
|
+
jobs_data = JSON.parse(jobs_response.body)
|
49
|
+
members = jobs_data["Members"] || []
|
50
|
+
|
51
|
+
if members.empty?
|
52
|
+
puts "No jobs to clear.".yellow
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "Clearing #{members.length} jobs...".yellow
|
57
|
+
|
58
|
+
members.each_with_index do |job, i|
|
59
|
+
puts "Removing #{job['Id']} : #{job['JobState']} > #{job['Message']} [#{i+1}/#{members.count}]".yellow
|
60
|
+
response = authenticated_request(:delete, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{job['Id']}")
|
61
|
+
|
62
|
+
unless response.status.between?(200, 299)
|
63
|
+
puts "Warning: Failed to delete job #{job['Id']}. Status code: #{response.status}".yellow
|
68
64
|
end
|
69
|
-
else
|
70
|
-
raise Error, "Failed to get jobs. Status code: #{jobs_response.status}"
|
71
65
|
end
|
66
|
+
|
67
|
+
puts "Successfully cleared all jobs".green
|
68
|
+
true
|
72
69
|
end
|
73
70
|
|
74
71
|
# Force clear the job queue
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module IDRAC
|
6
|
+
module Network
|
7
|
+
def get_bmc_network
|
8
|
+
# Get the iDRAC ethernet interface
|
9
|
+
collection_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/EthernetInterfaces")
|
10
|
+
|
11
|
+
if collection_response.status == 200
|
12
|
+
collection = JSON.parse(collection_response.body)
|
13
|
+
|
14
|
+
if collection["Members"] && collection["Members"].any?
|
15
|
+
interface_path = collection["Members"][0]["@odata.id"]
|
16
|
+
response = authenticated_request(:get, interface_path)
|
17
|
+
|
18
|
+
if response.status == 200
|
19
|
+
data = JSON.parse(response.body)
|
20
|
+
{
|
21
|
+
"ipv4" => data.dig("IPv4Addresses", 0, "Address"),
|
22
|
+
"mask" => data.dig("IPv4Addresses", 0, "SubnetMask"),
|
23
|
+
"gateway" => data.dig("IPv4Addresses", 0, "Gateway"),
|
24
|
+
"mode" => data.dig("IPv4Addresses", 0, "AddressOrigin"), # DHCP or Static
|
25
|
+
"mac" => data["MACAddress"],
|
26
|
+
"hostname" => data["HostName"],
|
27
|
+
"fqdn" => data["FQDN"],
|
28
|
+
"dns_servers" => data["NameServers"] || [],
|
29
|
+
"name" => data["Id"] || "iDRAC",
|
30
|
+
"speed_mbps" => data["SpeedMbps"] || 1000,
|
31
|
+
"status" => data.dig("Status", "Health") || "OK",
|
32
|
+
"kind" => "ethernet"
|
33
|
+
}
|
34
|
+
else
|
35
|
+
raise Error, "Failed to get interface details. Status: #{response.status}"
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise Error, "No ethernet interfaces found"
|
39
|
+
end
|
40
|
+
else
|
41
|
+
raise Error, "Failed to get ethernet interfaces. Status: #{collection_response.status}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_bmc_network(ipv4: nil, mask: nil, gateway: nil,
|
46
|
+
dns_primary: nil, dns_secondary: nil, hostname: nil,
|
47
|
+
dhcp: false)
|
48
|
+
# Get the interface path first
|
49
|
+
collection_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/EthernetInterfaces")
|
50
|
+
|
51
|
+
if collection_response.status == 200
|
52
|
+
collection = JSON.parse(collection_response.body)
|
53
|
+
|
54
|
+
if collection["Members"] && collection["Members"].any?
|
55
|
+
interface_path = collection["Members"][0]["@odata.id"]
|
56
|
+
|
57
|
+
if dhcp
|
58
|
+
puts "Setting iDRAC to DHCP mode...".yellow
|
59
|
+
body = {
|
60
|
+
"DHCPv4" => {
|
61
|
+
"DHCPEnabled" => true
|
62
|
+
},
|
63
|
+
"IPv4Addresses" => [{
|
64
|
+
"AddressOrigin" => "DHCP"
|
65
|
+
}]
|
66
|
+
}
|
67
|
+
else
|
68
|
+
puts "Configuring iDRAC network settings...".yellow
|
69
|
+
body = {}
|
70
|
+
|
71
|
+
# Configure static IP if provided
|
72
|
+
if ipv4 && mask
|
73
|
+
body["IPv4Addresses"] = [{
|
74
|
+
"Address" => ipv4,
|
75
|
+
"SubnetMask" => mask,
|
76
|
+
"Gateway" => gateway,
|
77
|
+
"AddressOrigin" => "Static"
|
78
|
+
}]
|
79
|
+
puts " IP: #{ipv4}/#{mask}".cyan
|
80
|
+
puts " Gateway: #{gateway}".cyan if gateway
|
81
|
+
end
|
82
|
+
|
83
|
+
# Configure DNS if provided
|
84
|
+
if dns_primary || dns_secondary
|
85
|
+
dns_servers = []
|
86
|
+
dns_servers << dns_primary if dns_primary
|
87
|
+
dns_servers << dns_secondary if dns_secondary
|
88
|
+
body["StaticNameServers"] = dns_servers
|
89
|
+
puts " DNS: #{dns_servers.join(', ')}".cyan
|
90
|
+
end
|
91
|
+
|
92
|
+
# Configure hostname if provided
|
93
|
+
if hostname
|
94
|
+
body["HostName"] = hostname
|
95
|
+
puts " Hostname: #{hostname}".cyan
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
response = authenticated_request(
|
100
|
+
:patch,
|
101
|
+
interface_path,
|
102
|
+
body: body.to_json,
|
103
|
+
headers: { 'Content-Type' => 'application/json' }
|
104
|
+
)
|
105
|
+
|
106
|
+
if response.status.between?(200, 299)
|
107
|
+
puts "iDRAC network configured successfully.".green
|
108
|
+
puts "WARNING: iDRAC may restart network services. Connection may be lost.".yellow if ip_address
|
109
|
+
true
|
110
|
+
else
|
111
|
+
raise Error, "Failed to configure iDRAC network: #{response.status} - #{response.body}"
|
112
|
+
end
|
113
|
+
else
|
114
|
+
raise Error, "No ethernet interfaces found"
|
115
|
+
end
|
116
|
+
else
|
117
|
+
raise Error, "Failed to get ethernet interfaces"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_bmc_dhcp
|
122
|
+
# Convenience method
|
123
|
+
set_bmc_network(dhcp: true)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/idrac/storage.rb
CHANGED
@@ -13,7 +13,8 @@ module IDRAC
|
|
13
13
|
|
14
14
|
# Transform and return all controllers as an array of hashes with string keys
|
15
15
|
controllers = data["Members"].map do |controller|
|
16
|
-
{
|
16
|
+
controller_data = {
|
17
|
+
"id" => controller["Id"],
|
17
18
|
"name" => controller["Name"],
|
18
19
|
"model" => controller["Model"],
|
19
20
|
"drives_count" => controller["Drives"].size,
|
@@ -23,9 +24,21 @@ module IDRAC
|
|
23
24
|
"encryption_capability" => controller.dig("Oem", "Dell", "DellController", "EncryptionCapability"),
|
24
25
|
"controller_type" => controller.dig("Oem", "Dell", "DellController", "ControllerType"),
|
25
26
|
"pci_slot" => controller.dig("Oem", "Dell", "DellController", "PCISlot"),
|
26
|
-
"
|
27
|
-
|
27
|
+
"@odata.id" => controller["@odata.id"],
|
28
|
+
# Store full controller data for access to all fields
|
29
|
+
"Oem" => controller["Oem"],
|
30
|
+
"Status" => controller["Status"],
|
31
|
+
"StorageControllers" => controller["StorageControllers"]
|
28
32
|
}
|
33
|
+
|
34
|
+
# Fetch drives for this controller
|
35
|
+
if controller["Drives"] && !controller["Drives"].empty?
|
36
|
+
controller_data["drives"] = fetch_controller_drives(controller["@odata.id"])
|
37
|
+
else
|
38
|
+
controller_data["drives"] = []
|
39
|
+
end
|
40
|
+
|
41
|
+
controller_data
|
29
42
|
end
|
30
43
|
|
31
44
|
return controllers.sort_by { |c| c["name"] }
|
@@ -36,6 +49,56 @@ module IDRAC
|
|
36
49
|
raise Error, "Failed to get controllers. Status code: #{response.status}"
|
37
50
|
end
|
38
51
|
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def fetch_controller_drives(controller_id)
|
56
|
+
controller_path = controller_id.split("v1/").last
|
57
|
+
response = authenticated_request(:get, "/redfish/v1/#{controller_path}?$expand=*($levels=1)")
|
58
|
+
|
59
|
+
if response.status == 200
|
60
|
+
begin
|
61
|
+
data = JSON.parse(response.body)
|
62
|
+
|
63
|
+
drives = data["Drives"].map do |body|
|
64
|
+
serial = body["SerialNumber"]
|
65
|
+
serial = body["Identifiers"].first["DurableName"] if serial.blank?
|
66
|
+
{
|
67
|
+
"id" => body["Id"],
|
68
|
+
"name" => body["Name"],
|
69
|
+
"serial" => serial,
|
70
|
+
"manufacturer" => body["Manufacturer"],
|
71
|
+
"model" => body["Model"],
|
72
|
+
"revision" => body["Revision"],
|
73
|
+
"capacity_bytes" => body["CapacityBytes"],
|
74
|
+
"speed_gbps" => body["CapableSpeedGbs"],
|
75
|
+
"rotation_speed_rpm" => body["RotationSpeedRPM"],
|
76
|
+
"media_type" => body["MediaType"],
|
77
|
+
"protocol" => body["Protocol"],
|
78
|
+
"health" => body.dig("Status", "Health") || "N/A",
|
79
|
+
"temperature_celsius" => nil, # Not available in standard iDRAC
|
80
|
+
"failure_predicted" => body["FailurePredicted"],
|
81
|
+
"life_left_percent" => body["PredictedMediaLifeLeftPercent"],
|
82
|
+
# Dell-specific fields
|
83
|
+
"certified" => body.dig("Oem", "Dell", "DellPhysicalDisk", "Certified"),
|
84
|
+
"raid_status" => body.dig("Oem", "Dell", "DellPhysicalDisk", "RaidStatus"),
|
85
|
+
"operation_name" => body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationName"),
|
86
|
+
"operation_progress" => body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationPercentCompletePercent"),
|
87
|
+
"encryption_ability" => body["EncryptionAbility"],
|
88
|
+
"odata_id" => body["@odata.id"] # Full API path for operations
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
return drives.sort_by { |d| d["name"] }
|
93
|
+
rescue JSON::ParserError
|
94
|
+
[]
|
95
|
+
end
|
96
|
+
else
|
97
|
+
[]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
public
|
39
102
|
|
40
103
|
# Find the best controller based on preference flags
|
41
104
|
# @param name_pattern [String] Regex pattern to match controller name (defaults to "PERC")
|
@@ -108,22 +171,28 @@ module IDRAC
|
|
108
171
|
serial = body["SerialNumber"]
|
109
172
|
serial = body["Identifiers"].first["DurableName"] if serial.blank?
|
110
173
|
{
|
174
|
+
"id" => body["Id"],
|
175
|
+
"name" => body["Name"],
|
111
176
|
"serial" => serial,
|
177
|
+
"manufacturer" => body["Manufacturer"],
|
112
178
|
"model" => body["Model"],
|
113
|
-
"
|
179
|
+
"revision" => body["Revision"],
|
114
180
|
"capacity_bytes" => body["CapacityBytes"],
|
115
|
-
"
|
116
|
-
"
|
117
|
-
"manufacturer" => body["Manufacturer"],
|
181
|
+
"speed_gbps" => body["CapableSpeedGbs"],
|
182
|
+
"rotation_speed_rpm" => body["RotationSpeedRPM"],
|
118
183
|
"media_type" => body["MediaType"],
|
184
|
+
"protocol" => body["Protocol"],
|
185
|
+
"health" => body.dig("Status", "Health") || "N/A",
|
186
|
+
"temperature_celsius" => nil, # Not available in standard iDRAC
|
119
187
|
"failure_predicted" => body["FailurePredicted"],
|
120
188
|
"life_left_percent" => body["PredictedMediaLifeLeftPercent"],
|
189
|
+
# Dell-specific fields
|
121
190
|
"certified" => body.dig("Oem", "Dell", "DellPhysicalDisk", "Certified"),
|
122
191
|
"raid_status" => body.dig("Oem", "Dell", "DellPhysicalDisk", "RaidStatus"),
|
123
192
|
"operation_name" => body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationName"),
|
124
193
|
"operation_progress" => body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationPercentCompletePercent"),
|
125
194
|
"encryption_ability" => body["EncryptionAbility"],
|
126
|
-
"
|
195
|
+
"odata_id" => body["@odata.id"] # Full API path for operations
|
127
196
|
}
|
128
197
|
end
|
129
198
|
|
data/lib/idrac/system.rb
CHANGED
@@ -16,7 +16,7 @@ module IDRAC
|
|
16
16
|
bank, index = /DIMM ([A-Z])(\d+)/.match(dimm_name).captures
|
17
17
|
|
18
18
|
{
|
19
|
-
"
|
19
|
+
"manufacturer" => m["Manufacturer"],
|
20
20
|
"name" => m["Name"],
|
21
21
|
"capacity_bytes" => m["CapacityMiB"].to_i * 1024 * 1024,
|
22
22
|
"health" => m.dig("Status","Health") || "N/A",
|
@@ -24,7 +24,9 @@ module IDRAC
|
|
24
24
|
"part_number" => m["PartNumber"],
|
25
25
|
"serial" => m["SerialNumber"],
|
26
26
|
"bank" => bank,
|
27
|
-
"index" => index.to_i
|
27
|
+
"index" => index.to_i,
|
28
|
+
"memory_device_type" => m["MemoryDeviceType"],
|
29
|
+
"base_module_type" => m["BaseModuleType"]
|
28
30
|
}
|
29
31
|
end
|
30
32
|
|
@@ -87,7 +89,8 @@ module IDRAC
|
|
87
89
|
"name" => fan["Name"],
|
88
90
|
"rpm" => fan["Reading"],
|
89
91
|
"serial" => fan["SerialNumber"],
|
90
|
-
"status" => fan.dig("Status", "Health")
|
92
|
+
"status" => fan.dig("Status", "Health"),
|
93
|
+
"state" => fan.dig("Status", "State")
|
91
94
|
}
|
92
95
|
end
|
93
96
|
|
@@ -646,7 +649,7 @@ module IDRAC
|
|
646
649
|
data = JSON.parse(response.body)
|
647
650
|
|
648
651
|
health = {
|
649
|
-
"
|
652
|
+
"rollup" => data.dig("Status", "HealthRollup"),
|
650
653
|
"system" => data.dig("Status", "Health"),
|
651
654
|
"processor" => data.dig("ProcessorSummary", "Status", "Health"),
|
652
655
|
"memory" => data.dig("MemorySummary", "Status", "Health"),
|
@@ -666,17 +669,29 @@ module IDRAC
|
|
666
669
|
def system_event_logs
|
667
670
|
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Sel?$expand=*($levels=1)")
|
668
671
|
|
669
|
-
if response.status ==
|
672
|
+
if response.status == 404
|
673
|
+
debug "SEL log not available on this system", 1, :yellow
|
674
|
+
return []
|
675
|
+
elsif response.status == 200
|
670
676
|
begin
|
671
677
|
data = JSON.parse(response.body)
|
672
678
|
|
673
679
|
logs = data["Members"].map do |log|
|
674
680
|
puts "#{log['Id']} : #{log['Created']} : #{log['Message']} : #{log['Severity']}".yellow
|
675
|
-
|
681
|
+
{
|
682
|
+
"id" => log["Id"],
|
683
|
+
"name" => log["Name"],
|
684
|
+
"created" => log["Created"],
|
685
|
+
"severity" => log["Severity"],
|
686
|
+
"message" => log["Message"],
|
687
|
+
"message_id" => log["MessageId"],
|
688
|
+
"sensor_type" => log["SensorType"],
|
689
|
+
"sensor_number" => log["SensorNumber"]
|
690
|
+
}
|
676
691
|
end
|
677
692
|
|
678
693
|
# Sort by creation date, newest first
|
679
|
-
return logs.sort_by { |log| log['
|
694
|
+
return logs.sort_by { |log| log['created'] || "" }.reverse
|
680
695
|
rescue JSON::ParserError
|
681
696
|
raise Error, "Failed to parse system event logs response: #{response.body}"
|
682
697
|
end
|
@@ -684,6 +699,9 @@ module IDRAC
|
|
684
699
|
raise Error, "Failed to get system event logs. Status code: #{response.status}"
|
685
700
|
end
|
686
701
|
end
|
702
|
+
|
703
|
+
# Alias for consistency with Supermicro naming
|
704
|
+
alias_method :sel_log, :system_event_logs
|
687
705
|
|
688
706
|
# Clear system event logs
|
689
707
|
def clear_system_event_logs
|
@@ -710,6 +728,37 @@ module IDRAC
|
|
710
728
|
raise Error, error_message
|
711
729
|
end
|
712
730
|
end
|
731
|
+
|
732
|
+
# Alias for consistency with Supermicro naming
|
733
|
+
alias_method :clear_sel_log, :clear_system_event_logs
|
734
|
+
|
735
|
+
# SEL log summary - display recent entries
|
736
|
+
def sel_summary(limit: 10)
|
737
|
+
entries = sel_log
|
738
|
+
|
739
|
+
if entries.empty?
|
740
|
+
puts "No log entries found.".yellow
|
741
|
+
return entries
|
742
|
+
end
|
743
|
+
|
744
|
+
puts "Total entries: #{entries.length}".cyan
|
745
|
+
puts "\nMost recent #{limit} entries:".cyan
|
746
|
+
|
747
|
+
entries.first(limit).each do |entry|
|
748
|
+
severity_color = case entry["severity"]
|
749
|
+
when "Critical" then :red
|
750
|
+
when "Warning" then :yellow
|
751
|
+
when "OK" then :green
|
752
|
+
else :white
|
753
|
+
end
|
754
|
+
|
755
|
+
puts "\n[#{entry['created']}] #{entry['severity']}".send(severity_color)
|
756
|
+
puts " #{entry['message']}"
|
757
|
+
puts " ID: #{entry['id']} | MessageID: #{entry['message_id']}" if entry['message_id']
|
758
|
+
end
|
759
|
+
|
760
|
+
entries
|
761
|
+
end
|
713
762
|
|
714
763
|
# Get total memory in human-readable format
|
715
764
|
def total_memory_human(memory_data)
|
data/lib/idrac/version.rb
CHANGED
data/lib/idrac/virtual_media.rb
CHANGED
@@ -215,33 +215,6 @@ module IDRAC
|
|
215
215
|
end
|
216
216
|
end
|
217
217
|
|
218
|
-
# Get current boot source override settings
|
219
|
-
def get_boot_source_override
|
220
|
-
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
221
|
-
|
222
|
-
if response.status == 200
|
223
|
-
begin
|
224
|
-
data = JSON.parse(response.body)
|
225
|
-
boot = data["Boot"]
|
226
|
-
|
227
|
-
puts "Boot Source Override Configuration:".green
|
228
|
-
puts " Enabled: #{boot['BootSourceOverrideEnabled']}"
|
229
|
-
puts " Target: #{boot['BootSourceOverrideTarget']}"
|
230
|
-
puts " Mode: #{boot['BootSourceOverrideMode']}" if boot['BootSourceOverrideMode']
|
231
|
-
|
232
|
-
if boot["BootSourceOverrideEnabled"] != "Once" || boot["BootSourceOverrideTarget"] == "None"
|
233
|
-
return "None"
|
234
|
-
else
|
235
|
-
return "#{boot['BootSourceOverrideMode']} #{boot['BootSourceOverrideTarget']}"
|
236
|
-
end
|
237
|
-
rescue JSON::ParserError
|
238
|
-
raise Error, "Failed to parse boot source response: #{response.body}"
|
239
|
-
end
|
240
|
-
else
|
241
|
-
raise Error, "Failed to get boot source override. Status code: #{response.status}"
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
218
|
private
|
246
219
|
|
247
220
|
|
data/lib/idrac.rb
CHANGED
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.8.
|
4
|
+
version: 0.8.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
@@ -254,6 +254,7 @@ files:
|
|
254
254
|
- README.md
|
255
255
|
- bin/console
|
256
256
|
- bin/idrac
|
257
|
+
- bin/idrac-tsr
|
257
258
|
- bin/setup
|
258
259
|
- idrac.gemspec
|
259
260
|
- lib/idrac.rb
|
@@ -265,6 +266,7 @@ files:
|
|
265
266
|
- lib/idrac/jobs.rb
|
266
267
|
- lib/idrac/license.rb
|
267
268
|
- lib/idrac/lifecycle.rb
|
269
|
+
- lib/idrac/network.rb
|
268
270
|
- lib/idrac/power.rb
|
269
271
|
- lib/idrac/session.rb
|
270
272
|
- lib/idrac/storage.rb
|