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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f05569a76e03f75c446229de12ff12a84b6d93a23256821e3fba8c43e2241a49
4
- data.tar.gz: 25de5109e5e40914d50d7ded48a2e83fb42f93f1fe1a3fa1b83fdd307fe15c58
3
+ metadata.gz: 6f2dd002b1876905499180d33abb476ed732f945aef7df82ed55bc68887763fd
4
+ data.tar.gz: a6a90ac44186a45ca043af595a2797d992d998b9da1a75a2d569c1ea2c7f0c1e
5
5
  SHA512:
6
- metadata.gz: d28f789451cddfaa2fbd31a24109f6a7517b4605a1604c70c37bba5cc9b73d10bd3f187fcdc5e934d55c06c966bcc7cacbad615ffadca6fb873d6e03af0284bf
7
- data.tar.gz: 22b385944dd4abb637bdfea5f3383f9b853d6bd37ffa889fde70482802da163cdd20ef1f7580e7c54657aa900fbf9cbb8ef8e7199fcd0f4d4a985617f574c158
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 BIOS boot options
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
- handle_response(jobs_response)
46
+ return true unless jobs_response.status == 200
48
47
 
49
- if jobs_response.status == 200
50
- begin
51
- jobs_data = JSON.parse(jobs_response.body)
52
- members = jobs_data["Members"]
53
-
54
- # Delete each job individually
55
- members.each.with_index do |job, i|
56
- puts "Removing #{job['Id']} : #{job['JobState']} > #{job['Message']} [#{i+1}/#{members.count}]".yellow
57
- delete_response = authenticated_request(:delete, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{job['Id']}")
58
-
59
- unless delete_response.status.between?(200, 299)
60
- puts "Warning: Failed to delete job #{job['Id']}. Status code: #{delete_response.status}".yellow
61
- end
62
- end
63
-
64
- puts "Successfully cleared all jobs".green
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
- "raw" => controller,
27
- "@odata.id" => controller["@odata.id"]
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
- "name" => body["Name"],
179
+ "revision" => body["Revision"],
114
180
  "capacity_bytes" => body["CapacityBytes"],
115
- "health" => body.dig("Status", "Health") || "N/A",
116
- "speed_gbp" => body["CapableSpeedGbs"],
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
- "@odata.id" => body["@odata.id"]
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
- "model" => m["Model"],
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
- "overall" => data.dig("Status", "HealthRollup"),
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 == 200
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
- log
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['Created'] }.reverse
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.8.2"
4
+ VERSION = "0.8.4"
5
5
  end
@@ -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
@@ -59,5 +59,6 @@ require 'idrac/boot'
59
59
  require 'idrac/license'
60
60
  require 'idrac/system_config'
61
61
  require 'idrac/utility'
62
+ require 'idrac/network'
62
63
  # Client include must come last:
63
64
  require 'idrac/client'
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.2
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