supermicro 0.1.6 → 0.1.8

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.
@@ -6,28 +6,49 @@ require 'colorize'
6
6
  module Supermicro
7
7
  module Jobs
8
8
  def jobs
9
- response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks?$expand=*($levels=1)")
9
+ tasks = jobs_detail
10
+
11
+ # Return summary format consistent with iDRAC
12
+ {
13
+ completed_count: tasks.count { |t| t["state"] == "Completed" },
14
+ incomplete_count: tasks.count { |t| t["state"] != "Completed" },
15
+ total_count: tasks.count
16
+ }
17
+ end
18
+
19
+ def jobs_detail
20
+ response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks")
10
21
 
11
22
  if response.status == 200
12
23
  begin
13
24
  data = JSON.parse(response.body)
25
+ members = data["Members"] || []
14
26
 
15
- tasks = data["Members"]&.map do |task|
16
- {
17
- "id" => task["Id"],
18
- "name" => task["Name"],
19
- "state" => task["TaskState"],
20
- "status" => task["TaskStatus"],
21
- "percent_complete" => task["PercentComplete"] || task.dig("Oem", "Supermicro", "PercentComplete"),
22
- "start_time" => task["StartTime"],
23
- "end_time" => task["EndTime"],
24
- "messages" => task["Messages"]
25
- }
26
- end || []
27
+ # Supermicro doesn't support expand, so fetch each task individually
28
+ tasks = members.map do |member|
29
+ task_id = member["@odata.id"].split('/').last
30
+ task_response = authenticated_request(:get, member["@odata.id"])
31
+
32
+ if task_response.status == 200
33
+ task = JSON.parse(task_response.body)
34
+ {
35
+ "id" => task["Id"],
36
+ "name" => task["Name"],
37
+ "state" => task["TaskState"],
38
+ "status" => task["TaskStatus"],
39
+ "percent_complete" => task["PercentComplete"],
40
+ "start_time" => task["StartTime"],
41
+ "end_time" => task["EndTime"],
42
+ "messages" => task["Messages"]
43
+ }
44
+ else
45
+ nil
46
+ end
47
+ end.compact
27
48
 
28
49
  return tasks
29
50
  rescue JSON::ParserError
30
- raise Error, "Failed to parse tasks response: #{response.body}"
51
+ raise Error, "Failed to parse tasks response"
31
52
  end
32
53
  else
33
54
  []
@@ -116,42 +137,49 @@ module Supermicro
116
137
  end
117
138
  end
118
139
 
119
- def clear_completed_jobs
120
- all_jobs = jobs
121
- completed = all_jobs.select { |j| j["state"] == "Completed" }
140
+ def clear_jobs!
141
+ # Note: Supermicro doesn't actually delete tasks - DELETE just marks them as "Killed"
142
+ # The BMC maintains a rolling buffer of tasks (typically ~28-30) with oldest being overwritten
143
+ # This method will "kill" any running tasks but won't remove them from the list
122
144
 
123
- if completed.empty?
124
- puts "No completed jobs to clear.".yellow
145
+ response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks")
146
+ return true unless response.status == 200
147
+
148
+ data = JSON.parse(response.body)
149
+ members = data["Members"] || []
150
+
151
+ if members.empty?
152
+ puts "No jobs to clear.".yellow
125
153
  return true
126
154
  end
127
155
 
128
- puts "Clearing #{completed.length} completed jobs...".yellow
129
-
130
- success = true
131
- completed.each do |job|
132
- begin
133
- response = authenticated_request(
134
- :delete,
135
- "/redfish/v1/TaskService/Tasks/#{job["id"]}"
136
- )
137
-
138
- if response.status.between?(200, 299)
139
- puts " Cleared: #{job["name"]} (#{job["id"]})".green
140
- else
141
- puts " Failed to clear: #{job["name"]} (#{job["id"]})".red
142
- success = false
156
+ # Only try to kill tasks that are actually running
157
+ running_count = 0
158
+ members.each do |member|
159
+ task_id = member["@odata.id"].split('/').last
160
+ task_response = authenticated_request(:get, member["@odata.id"])
161
+
162
+ if task_response.status == 200
163
+ task = JSON.parse(task_response.body)
164
+ if ["Running", "Starting", "New", "Pending"].include?(task["TaskState"])
165
+ running_count += 1
166
+ puts "Killing task #{task_id}: #{task['Name']} (#{task['TaskState']})".yellow
167
+ authenticated_request(:delete, member["@odata.id"])
143
168
  end
144
- rescue => e
145
- puts " Error clearing #{job["id"]}: #{e.message}".red
146
- success = false
147
169
  end
148
170
  end
149
171
 
150
- success
172
+ if running_count > 0
173
+ puts "Killed #{running_count} running tasks.".green
174
+ else
175
+ puts "No running tasks to kill (#{members.length} completed/killed tasks remain in history).".yellow
176
+ end
177
+
178
+ true
151
179
  end
152
180
 
153
181
  def jobs_summary
154
- all_jobs = jobs
182
+ all_jobs = jobs_detail
155
183
 
156
184
  puts "\n=== Jobs Summary ===".green
157
185
 
@@ -11,23 +11,27 @@ module Supermicro
11
11
  if response.status == 200
12
12
  data = JSON.parse(response.body)
13
13
  {
14
- "ipv4_address" => data.dig("IPv4Addresses", 0, "Address"),
15
- "subnet_mask" => data.dig("IPv4Addresses", 0, "SubnetMask"),
14
+ "ipv4" => data.dig("IPv4Addresses", 0, "Address"),
15
+ "mask" => data.dig("IPv4Addresses", 0, "SubnetMask"),
16
16
  "gateway" => data.dig("IPv4Addresses", 0, "Gateway"),
17
17
  "mode" => data.dig("IPv4Addresses", 0, "AddressOrigin"), # DHCP or Static
18
- "mac_address" => data["MACAddress"],
18
+ "mac" => data["MACAddress"],
19
19
  "hostname" => data["HostName"],
20
20
  "fqdn" => data["FQDN"],
21
- "dns_servers" => data["NameServers"] || []
21
+ "dns_servers" => data["NameServers"] || [],
22
+ "name" => data["Id"] || "BMC",
23
+ "speed_mbps" => data["SpeedMbps"] || 1000,
24
+ "status" => data.dig("Status", "Health") || "OK",
25
+ "kind" => "ethernet"
22
26
  }
23
27
  else
24
28
  raise Error, "Failed to get BMC network config. Status: #{response.status}"
25
29
  end
26
30
  end
27
31
 
28
- def set_bmc_network(ip_address: nil, subnet_mask: nil, gateway: nil,
32
+ def set_bmc_network(ipv4: nil, mask: nil, gateway: nil,
29
33
  dns_primary: nil, dns_secondary: nil, hostname: nil,
30
- dhcp: false)
34
+ dhcp: false, wait: true)
31
35
 
32
36
  if dhcp
33
37
  puts "Setting BMC to DHCP mode...".yellow
@@ -41,13 +45,15 @@ module Supermicro
41
45
  body = {}
42
46
 
43
47
  # Configure static IP if provided
44
- if ip_address && subnet_mask
48
+ if ipv4 && mask
49
+ # Must explicitly disable DHCP when setting static IP
50
+ body["DHCPv4"] = { "DHCPEnabled" => false }
45
51
  body["IPv4StaticAddresses"] = [{
46
- "Address" => ip_address,
47
- "SubnetMask" => subnet_mask,
52
+ "Address" => ipv4,
53
+ "SubnetMask" => mask,
48
54
  "Gateway" => gateway
49
55
  }]
50
- puts " IP: #{ip_address}/#{subnet_mask}".cyan
56
+ puts " IP: #{ipv4}/#{mask}".cyan
51
57
  puts " Gateway: #{gateway}".cyan if gateway
52
58
  end
53
59
 
@@ -75,9 +81,43 @@ module Supermicro
75
81
  )
76
82
 
77
83
  if response.status.between?(200, 299)
78
- puts "BMC network configured successfully.".green
79
- puts "WARNING: BMC may restart network services. Connection may be lost.".yellow if ip_address
80
- true
84
+ puts "BMC network configuration submitted.".green
85
+
86
+ # If we're changing IP and wait is enabled, handle the transition
87
+ if ip_address && wait
88
+ puts "Waiting for BMC to apply network changes...".yellow
89
+
90
+ # Check if response contains a task/job ID
91
+ if response.status == 202 && response.headers['Location']
92
+ # Job was created, monitor it
93
+ job_uri = response.headers['Location']
94
+ puts "Monitoring job: #{job_uri}".cyan
95
+
96
+ # Wait for current job to complete (on old IP)
97
+ wait_for_network_job(job_uri)
98
+ else
99
+ # No job, just wait a bit for the change to apply
100
+ sleep 5
101
+ end
102
+
103
+ # Now verify the BMC is reachable on the new IP
104
+ puts "Verifying BMC is reachable on new IP: #{ip_address}...".yellow
105
+ if verify_bmc_on_new_ip(ip_address, @username, @password)
106
+ puts "BMC successfully configured and reachable on #{ip_address}".green
107
+
108
+ # Update our client's host to the new IP
109
+ @host = ip_address
110
+ true
111
+ else
112
+ puts "WARNING: BMC configuration may have succeeded but cannot reach BMC on #{ip_address}".yellow
113
+ puts "The BMC may still be applying changes or may require manual verification.".yellow
114
+ false
115
+ end
116
+ else
117
+ puts "BMC network configured successfully.".green
118
+ puts "WARNING: BMC may restart network services. Connection may be lost.".yellow if ip_address && !wait
119
+ true
120
+ end
81
121
  else
82
122
  raise Error, "Failed to configure BMC network: #{response.status} - #{response.body}"
83
123
  end
@@ -87,5 +127,91 @@ module Supermicro
87
127
  # Convenience method
88
128
  set_bmc_network(dhcp: true)
89
129
  end
130
+
131
+ private
132
+
133
+ def wait_for_network_job(job_uri, timeout: 60)
134
+ start_time = Time.now
135
+
136
+ while (Time.now - start_time) < timeout
137
+ begin
138
+ response = authenticated_request(:get, job_uri)
139
+
140
+ if response.status == 200
141
+ data = JSON.parse(response.body)
142
+ state = data["TaskState"] || data["JobState"] || "Unknown"
143
+
144
+ case state
145
+ when "Completed", "OK"
146
+ puts "Network configuration job completed successfully.".green
147
+ return true
148
+ when "Exception", "Critical", "Error", "Failed"
149
+ puts "Network configuration job failed: #{state}".red
150
+ return false
151
+ else
152
+ print "."
153
+ sleep 2
154
+ end
155
+ else
156
+ # Can't check status, assume it's applying
157
+ sleep 2
158
+ end
159
+ rescue => e
160
+ # Connection might be interrupted during network change
161
+ debug "Connection error during job monitoring (expected): #{e.message}", 2
162
+ sleep 2
163
+ end
164
+ end
165
+
166
+ puts "\nJob monitoring timed out after #{timeout} seconds".yellow
167
+ false
168
+ end
169
+
170
+ def verify_bmc_on_new_ip(new_ip, username, password, retries: 10, delay: 3)
171
+ retries.times do |i|
172
+ begin
173
+ # Create a new connection to test the new IP
174
+ test_conn = Faraday.new(
175
+ url: "https://#{new_ip}",
176
+ ssl: { verify: @verify_ssl }
177
+ ) do |f|
178
+ f.adapter Faraday.default_adapter
179
+ f.response :follow_redirects
180
+ end
181
+
182
+ # Try to access the Redfish root
183
+ response = test_conn.get('/redfish/v1/')
184
+
185
+ if response.status == 200 || response.status == 401
186
+ # BMC is responding, try to login
187
+ login_response = test_conn.post do |req|
188
+ req.url '/redfish/v1/SessionService/Sessions'
189
+ req.headers['Content-Type'] = 'application/json'
190
+ req.body = {
191
+ "UserName" => username,
192
+ "Password" => password
193
+ }.to_json
194
+ end
195
+
196
+ if login_response.status.between?(200, 204)
197
+ # Successfully reached and authenticated
198
+ # Clean up the test session
199
+ if login_response.headers['x-auth-token']
200
+ test_conn.delete('/redfish/v1/SessionService/Sessions') do |req|
201
+ req.headers['X-Auth-Token'] = login_response.headers['x-auth-token']
202
+ end
203
+ end
204
+ return true
205
+ end
206
+ end
207
+ rescue => e
208
+ debug "Attempt #{i+1}/#{retries} failed: #{e.message}", 2
209
+ end
210
+
211
+ sleep delay unless i == retries - 1
212
+ end
213
+
214
+ false
215
+ end
90
216
  end
91
217
  end
@@ -6,14 +6,28 @@ require 'colorize'
6
6
  module Supermicro
7
7
  module Storage
8
8
  def storage_controllers
9
- response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage?$expand=*($levels=1)")
9
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage")
10
10
 
11
11
  if response.status == 200
12
12
  begin
13
13
  data = JSON.parse(response.body)
14
14
 
15
- controllers = data["Members"].map do |controller|
16
- {
15
+ controllers = data["Members"].map do |member|
16
+ # If member is just a reference, fetch the full data
17
+ if member["@odata.id"] && !member["Id"]
18
+ controller_path = member["@odata.id"]
19
+ controller_response = authenticated_request(:get, controller_path)
20
+
21
+ if controller_response.status == 200
22
+ controller = JSON.parse(controller_response.body)
23
+ else
24
+ next nil
25
+ end
26
+ else
27
+ controller = member
28
+ end
29
+
30
+ controller_data = {
17
31
  "id" => controller["Id"],
18
32
  "name" => controller["Name"],
19
33
  "status" => controller.dig("Status", "Health") || "N/A",
@@ -28,7 +42,16 @@ module Supermicro
28
42
  }
29
43
  }
30
44
  }
31
- end
45
+
46
+ # Fetch drives for this controller
47
+ if controller["Drives"] && !controller["Drives"].empty?
48
+ controller_data["drives"] = fetch_controller_drives(controller["Id"], controller["Drives"])
49
+ else
50
+ controller_data["drives"] = []
51
+ end
52
+
53
+ controller_data
54
+ end.compact
32
55
 
33
56
  return controllers
34
57
  rescue JSON::ParserError
@@ -38,94 +61,158 @@ module Supermicro
38
61
  raise Error, "Failed to get storage controllers. Status code: #{response.status}"
39
62
  end
40
63
  end
64
+
65
+ private
66
+
67
+ def fetch_controller_drives(controller_id, drive_refs)
68
+ drives = []
69
+
70
+ drive_refs.each do |drive_ref|
71
+ drive_path = drive_ref["@odata.id"]
72
+ drive_response = authenticated_request(:get, drive_path)
73
+
74
+ if drive_response.status == 200
75
+ drive_data = JSON.parse(drive_response.body)
76
+
77
+ drives << {
78
+ "id" => drive_data["Id"],
79
+ "name" => drive_data["Name"],
80
+ "serial" => drive_data["SerialNumber"],
81
+ "manufacturer" => drive_data["Manufacturer"],
82
+ "model" => drive_data["Model"],
83
+ "revision" => drive_data["Revision"],
84
+ "capacity_bytes" => drive_data["CapacityBytes"],
85
+ "speed_gbps" => drive_data["CapableSpeedGbs"] || drive_data["NegotiatedSpeedGbs"],
86
+ "rotation_speed_rpm" => drive_data["RotationSpeedRPM"],
87
+ "media_type" => drive_data["MediaType"],
88
+ "protocol" => drive_data["Protocol"],
89
+ "health" => drive_data.dig("Status", "Health") || "N/A",
90
+ "temperature_celsius" => drive_data.dig("Oem", "Supermicro", "Temperature"),
91
+ "failure_predicted" => drive_data["FailurePredicted"],
92
+ "life_left_percent" => drive_data["PredictedMediaLifeLeftPercent"]
93
+ }
94
+ end
95
+ end
96
+
97
+ drives
98
+ end
99
+
100
+ public
41
101
 
42
- def drives
43
- all_drives = []
102
+ def drives(controller_id)
103
+ # Following natural Redfish pattern - drives are scoped to a controller
104
+ raise ArgumentError, "Controller ID is required" unless controller_id
44
105
 
45
- controllers = storage_controllers
106
+ drives = []
46
107
 
47
- controllers.each do |controller|
48
- controller_id = controller["id"]
49
-
50
- response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage/#{controller_id}")
51
-
52
- if response.status == 200
53
- begin
54
- data = JSON.parse(response.body)
55
-
56
- if data["Drives"]
57
- data["Drives"].each do |drive_ref|
58
- drive_path = drive_ref["@odata.id"]
59
- drive_response = authenticated_request(:get, drive_path)
108
+ # Extract just the controller ID if given full path
109
+ controller_name = controller_id.split('/').last
110
+
111
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage/#{controller_name}")
112
+
113
+ if response.status == 200
114
+ begin
115
+ data = JSON.parse(response.body)
116
+
117
+ if data["Drives"]
118
+ data["Drives"].each do |drive_ref|
119
+ drive_path = drive_ref["@odata.id"]
120
+ drive_response = authenticated_request(:get, drive_path)
121
+
122
+ if drive_response.status == 200
123
+ drive_data = JSON.parse(drive_response.body)
60
124
 
61
- if drive_response.status == 200
62
- drive_data = JSON.parse(drive_response.body)
63
-
64
- all_drives << {
65
- "id" => drive_data["Id"],
66
- "name" => drive_data["Name"],
67
- "manufacturer" => drive_data["Manufacturer"],
68
- "model" => drive_data["Model"],
69
- "serial" => drive_data["SerialNumber"],
70
- "capacity_bytes" => drive_data["CapacityBytes"],
71
- "capacity_gb" => (drive_data["CapacityBytes"].to_f / 1_000_000_000).round(2),
72
- "protocol" => drive_data["Protocol"],
73
- "media_type" => drive_data["MediaType"],
74
- "status" => drive_data.dig("Status", "Health") || "N/A",
75
- "controller" => controller_id,
76
- "firmware_version" => drive_data["Revision"],
77
- "rotation_speed_rpm" => drive_data["RotationSpeedRPM"],
78
- "predicted_media_life_left_percent" => drive_data["PredictedMediaLifeLeftPercent"]
79
- }
80
- end
125
+ drives << {
126
+ "id" => drive_data["Id"],
127
+ "name" => drive_data["Name"],
128
+ "serial" => drive_data["SerialNumber"],
129
+ "manufacturer" => drive_data["Manufacturer"],
130
+ "model" => drive_data["Model"],
131
+ "revision" => drive_data["Revision"],
132
+ "capacity_bytes" => drive_data["CapacityBytes"],
133
+ "capacity_gb" => (drive_data["CapacityBytes"].to_f / (1000**3)).round(2),
134
+ "speed_gbps" => drive_data["CapableSpeedGbs"] || drive_data["NegotiatedSpeedGbs"],
135
+ "rotation_speed_rpm" => drive_data["RotationSpeedRPM"],
136
+ "media_type" => drive_data["MediaType"],
137
+ "protocol" => drive_data["Protocol"],
138
+ "status" => drive_data.dig("Status", "Health") || "N/A",
139
+ "health" => drive_data.dig("Status", "Health") || "N/A",
140
+ "temperature_celsius" => drive_data.dig("Oem", "Supermicro", "Temperature"),
141
+ "failure_predicted" => drive_data["FailurePredicted"],
142
+ "life_left_percent" => drive_data["PredictedMediaLifeLeftPercent"],
143
+ "certified" => drive_data.dig("Oem", "Supermicro", "Certified"),
144
+ "@odata.id" => drive_data["@odata.id"]
145
+ }
81
146
  end
82
147
  end
83
- rescue JSON::ParserError
84
- debug "Failed to parse storage data for controller #{controller_id}", 1, :yellow
85
148
  end
149
+ rescue JSON::ParserError
150
+ debug "Failed to parse storage data for controller #{controller_name}", 1, :yellow
86
151
  end
152
+ else
153
+ raise Error, "Failed to get drives for controller #{controller_name}. Status: #{response.status}"
87
154
  end
88
155
 
89
- return all_drives
156
+ return drives
90
157
  end
91
158
 
92
- def volumes
93
- all_volumes = []
159
+ def volumes(controller_id)
160
+ # Following natural Redfish pattern - volumes are scoped to a controller
161
+ raise ArgumentError, "Controller ID is required" unless controller_id
94
162
 
95
- controllers = storage_controllers
163
+ volumes = []
96
164
 
97
- controllers.each do |controller|
98
- controller_id = controller["id"]
99
-
100
- response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage/#{controller_id}/Volumes?$expand=*($levels=1)")
101
-
102
- if response.status == 200
103
- begin
104
- data = JSON.parse(response.body)
105
-
106
- if data["Members"]
107
- data["Members"].each do |volume|
108
- all_volumes << {
109
- "id" => volume["Id"],
110
- "name" => volume["Name"],
111
- "capacity_bytes" => volume["CapacityBytes"],
112
- "capacity_gb" => (volume["CapacityBytes"].to_f / 1_000_000_000).round(2),
113
- "volume_type" => volume["VolumeType"],
114
- "raid_type" => volume["RAIDType"],
115
- "status" => volume.dig("Status", "Health") || "N/A",
116
- "controller" => controller_id,
117
- "encrypted" => volume["Encrypted"],
118
- "optimum_io_size_bytes" => volume["OptimumIOSizeBytes"]
119
- }
165
+ # Extract just the controller ID if given full path
166
+ controller_name = controller_id.split('/').last
167
+
168
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage/#{controller_name}/Volumes")
169
+
170
+ if response.status == 200
171
+ begin
172
+ data = JSON.parse(response.body)
173
+
174
+ if data["Members"]
175
+ data["Members"].each do |volume_ref|
176
+ # Check if it's a reference or the actual volume data
177
+ if volume_ref["@odata.id"]
178
+ # It's a reference, fetch the volume
179
+ volume_path = volume_ref["@odata.id"]
180
+ volume_response = authenticated_request(:get, volume_path)
181
+
182
+ if volume_response.status == 200
183
+ volume = JSON.parse(volume_response.body)
184
+ else
185
+ next
186
+ end
187
+ else
188
+ # It's the actual volume data
189
+ volume = volume_ref
120
190
  end
191
+
192
+ volumes << {
193
+ "id" => volume["Id"],
194
+ "name" => volume["Name"],
195
+ "capacity_bytes" => volume["CapacityBytes"],
196
+ "capacity_gb" => (volume["CapacityBytes"].to_f / (1000**3)).round(2),
197
+ "volume_type" => volume["VolumeType"],
198
+ "raid_type" => volume["RAIDType"],
199
+ "status" => volume.dig("Status", "Health") || "N/A",
200
+ "health" => volume.dig("Status", "Health") || "N/A",
201
+ "encrypted" => volume["Encrypted"],
202
+ "optimum_io_size_bytes" => volume["OptimumIOSizeBytes"],
203
+ "@odata.id" => volume["@odata.id"]
204
+ }
121
205
  end
122
- rescue JSON::ParserError
123
- debug "Failed to parse volumes data for controller #{controller_id}", 1, :yellow
124
206
  end
207
+ rescue JSON::ParserError
208
+ debug "Failed to parse volumes data for controller #{controller_name}", 1, :yellow
125
209
  end
210
+ else
211
+ # Some controllers may not have volumes endpoint
212
+ debug "No volumes endpoint for controller #{controller_name} (Status: #{response.status})", 2, :yellow
126
213
  end
127
214
 
128
- return all_volumes
215
+ return volumes
129
216
  end
130
217
 
131
218
  def storage_summary
@@ -144,7 +231,13 @@ module Supermicro
144
231
  end
145
232
  end
146
233
 
147
- all_drives = drives
234
+ # Get all drives from all controllers
235
+ all_drives = []
236
+ controllers.each do |controller|
237
+ controller_drives = drives(controller["@odata.id"] || "/redfish/v1/Systems/1/Storage/#{controller['id']}")
238
+ all_drives.concat(controller_drives) if controller_drives
239
+ end
240
+
148
241
  puts "\nPhysical Drives:".cyan
149
242
  all_drives.each do |drive|
150
243
  puts " #{drive['name']} (#{drive['id']})".yellow
@@ -158,7 +251,13 @@ module Supermicro
158
251
  end
159
252
  end
160
253
 
161
- all_volumes = volumes
254
+ # Get all volumes from all controllers
255
+ all_volumes = []
256
+ controllers.each do |controller|
257
+ controller_volumes = volumes(controller["@odata.id"] || "/redfish/v1/Systems/1/Storage/#{controller['id']}")
258
+ all_volumes.concat(controller_volumes) if controller_volumes
259
+ end
260
+
162
261
  if all_volumes.any?
163
262
  puts "\nVolumes:".cyan
164
263
  all_volumes.each do |volume|