supermicro 0.1.7 → 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.
@@ -12,7 +12,19 @@ module Supermicro
12
12
  begin
13
13
  data = JSON.parse(response.body)
14
14
 
15
- memory = data["Members"].map do |m|
15
+ memory = data["Members"].map do |member|
16
+ # If member is just a reference, fetch the full data
17
+ if member["@odata.id"] && !member["CapacityMiB"]
18
+ mem_response = authenticated_request(:get, member["@odata.id"])
19
+ if mem_response.status == 200
20
+ m = JSON.parse(mem_response.body)
21
+ else
22
+ next nil
23
+ end
24
+ else
25
+ m = member
26
+ end
27
+
16
28
  next if m["CapacityMiB"].nil? || m["CapacityMiB"] == 0
17
29
 
18
30
  dimm_name = m["DeviceLocator"] || m["Name"]
@@ -26,16 +38,17 @@ module Supermicro
26
38
  end
27
39
 
28
40
  {
29
- "model" => m["Manufacturer"],
41
+ "manufacturer" => m["Manufacturer"],
30
42
  "name" => dimm_name,
31
43
  "capacity_bytes" => m["CapacityMiB"].to_i * 1024 * 1024,
32
44
  "health" => m.dig("Status", "Health") || "N/A",
33
- "speed_mhz" => m["OperatingSpeedMhz"],
45
+ "speed_mhz" => m["OperatingSpeedMHz"],
34
46
  "part_number" => m["PartNumber"],
35
47
  "serial" => m["SerialNumber"],
36
48
  "bank" => bank,
37
49
  "index" => index.to_i,
38
- "memory_type" => m["MemoryDeviceType"]
50
+ "memory_device_type" => m["MemoryDeviceType"],
51
+ "base_module_type" => m["BaseModuleType"]
39
52
  }
40
53
  end.compact
41
54
 
@@ -99,6 +112,7 @@ module Supermicro
99
112
  "name" => fan["Name"],
100
113
  "rpm" => rpm,
101
114
  "status" => health,
115
+ "state" => fan.dig("Status", "State"),
102
116
  "min_rpm" => fan["MinReadingRange"],
103
117
  "max_rpm" => fan["MaxReadingRange"]
104
118
  }
@@ -167,24 +181,52 @@ module Supermicro
167
181
  end
168
182
 
169
183
  def nics
170
- response = authenticated_request(:get, "/redfish/v1/Systems/1/EthernetInterfaces?$expand=*($levels=1)")
184
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/EthernetInterfaces")
171
185
 
172
186
  if response.status == 200
173
187
  begin
174
188
  data = JSON.parse(response.body)
175
189
 
176
- nics = data["Members"].map do |nic|
190
+ # If Members are not expanded, fetch each interface individually
191
+ nics = data["Members"].map do |member|
192
+ # Check if this is just a reference or full data
193
+ if member["@odata.id"] && !member["Id"]
194
+ # It's just a reference, fetch the full data
195
+ interface_response = authenticated_request(:get, member["@odata.id"])
196
+ if interface_response.status == 200
197
+ nic = JSON.parse(interface_response.body)
198
+ else
199
+ puts "Failed to fetch NIC #{member["@odata.id"]}: #{interface_response.status}".yellow
200
+ next
201
+ end
202
+ else
203
+ # We already have the full data
204
+ nic = member
205
+ end
206
+
207
+ # Create adapter structure to match iDRAC format
177
208
  {
178
- "id" => nic["Id"],
179
209
  "name" => nic["Name"],
180
- "mac" => nic["MACAddress"],
181
- "speed_mbps" => nic["SpeedMbps"],
182
- "status" => nic.dig("Status", "Health") || "N/A",
183
- "link_status" => nic["LinkStatus"],
184
- "ipv4" => nic.dig("IPv4Addresses", 0, "Address"),
185
- "ipv6" => nic.dig("IPv6Addresses", 0, "Address")
210
+ "manufacturer" => nic["Manufacturer"] || "Supermicro",
211
+ "model" => nil,
212
+ "part_number" => nil,
213
+ "serial" => nil,
214
+ "ports" => [
215
+ {
216
+ "name" => nic["Id"],
217
+ "status" => nic["LinkStatus"] == "LinkDown" ? "Down" : "Up",
218
+ "mac" => nic["MACAddress"],
219
+ "ipv4" => nic.dig("IPv4Addresses", 0, "Address"),
220
+ "mode" => nic.dig("IPv4Addresses", 0, "AddressOrigin"), # DHCP or Static
221
+ "mask" => nic.dig("IPv4Addresses", 0, "SubnetMask"),
222
+ "port" => 0,
223
+ "speed_mbps" => nic["SpeedMbps"] || 0,
224
+ "kind" => "ethernet",
225
+ "linux_device" => nil
226
+ }
227
+ ]
186
228
  }
187
- end
229
+ end.compact # Remove any nil entries from failed fetches
188
230
 
189
231
  return nics
190
232
  rescue JSON::ParserError
@@ -202,12 +244,21 @@ module Supermicro
202
244
  begin
203
245
  data = JSON.parse(response.body)
204
246
 
247
+ # Get Manager UUID for service tag (BMC MAC address)
248
+ manager_uuid = nil
249
+ manager_response = authenticated_request(:get, "/redfish/v1/Managers/1")
250
+ if manager_response.status == 200
251
+ manager_data = JSON.parse(manager_response.body)
252
+ manager_uuid = manager_data["UUID"]
253
+ end
254
+
205
255
  {
206
256
  "name" => data["Name"],
207
257
  "model" => data["Model"],
208
258
  "manufacturer" => data["Manufacturer"],
209
259
  "serial" => data["SerialNumber"],
210
260
  "uuid" => data["UUID"],
261
+ "manager_uuid" => manager_uuid,
211
262
  "bios_version" => data["BiosVersion"],
212
263
  "power_state" => data["PowerState"],
213
264
  "health" => data.dig("Status", "Health"),
@@ -254,6 +305,45 @@ module Supermicro
254
305
  data = power_consumption
255
306
  data["consumed_watts"] if data.is_a?(Hash)
256
307
  end
308
+
309
+ # Get system health status
310
+ def system_health
311
+ response = authenticated_request(:get, "/redfish/v1/Systems/1")
312
+
313
+ if response.status == 200
314
+ begin
315
+ data = JSON.parse(response.body)
316
+
317
+ health = {
318
+ "rollup" => data.dig("Status", "HealthRollup") || data.dig("Status", "Health"),
319
+ "system" => data.dig("Status", "Health"),
320
+ "processor" => data.dig("ProcessorSummary", "Status", "Health"),
321
+ "memory" => data.dig("MemorySummary", "Status", "Health")
322
+ }
323
+
324
+ # Try to get storage health
325
+ storage_response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage")
326
+ if storage_response.status == 200
327
+ storage_data = JSON.parse(storage_response.body)
328
+ if storage_data["Members"] && !storage_data["Members"].empty?
329
+ # Get first storage controller's health
330
+ storage_url = storage_data["Members"].first["@odata.id"]
331
+ storage_detail = authenticated_request(:get, storage_url)
332
+ if storage_detail.status == 200
333
+ storage_detail_data = JSON.parse(storage_detail.body)
334
+ health["storage"] = storage_detail_data.dig("Status", "Health")
335
+ end
336
+ end
337
+ end
338
+
339
+ health
340
+ rescue JSON::ParserError
341
+ raise Error, "Failed to parse system health information: #{response.body}"
342
+ end
343
+ else
344
+ raise Error, "Failed to get system health. Status code: #{response.status}"
345
+ end
346
+ end
257
347
 
258
348
  def temperatures
259
349
  response = authenticated_request(:get, "/redfish/v1/Chassis/1/Thermal")
@@ -5,81 +5,8 @@ require 'colorize'
5
5
 
6
6
  module Supermicro
7
7
  module SystemConfig
8
- def bios_attributes
9
- response = authenticated_request(:get, "/redfish/v1/Systems/1/Bios")
10
-
11
- if response.status == 200
12
- begin
13
- data = JSON.parse(response.body)
14
-
15
- {
16
- "attributes" => data["Attributes"],
17
- "attribute_registry" => data["AttributeRegistry"]
18
- }
19
- rescue JSON::ParserError
20
- raise Error, "Failed to parse BIOS attributes response: #{response.body}"
21
- end
22
- else
23
- raise Error, "Failed to get BIOS attributes. Status code: #{response.status}"
24
- end
25
- end
26
-
27
- def set_bios_attribute(attribute_name, value)
28
- puts "Setting BIOS attribute #{attribute_name} to #{value}...".yellow
29
-
30
- body = {
31
- "Attributes" => {
32
- attribute_name => value
33
- }
34
- }
35
-
36
- response = authenticated_request(
37
- :patch,
38
- "/redfish/v1/Systems/1/Bios/Settings",
39
- body: body.to_json,
40
- headers: { 'Content-Type': 'application/json' }
41
- )
42
-
43
- if response.status.between?(200, 299)
44
- puts "BIOS attribute set successfully. Changes will be applied on next reboot.".green
45
- return true
46
- else
47
- raise Error, "Failed to set BIOS attribute: #{response.status} - #{response.body}"
48
- end
49
- end
50
-
51
- def pending_bios_settings
52
- response = authenticated_request(:get, "/redfish/v1/Systems/1/Bios/Settings")
53
-
54
- if response.status == 200
55
- begin
56
- data = JSON.parse(response.body)
57
- data["Attributes"] || {}
58
- rescue JSON::ParserError
59
- raise Error, "Failed to parse pending BIOS settings response: #{response.body}"
60
- end
61
- else
62
- {}
63
- end
64
- end
65
-
66
- def reset_bios_defaults
67
- puts "Resetting BIOS to defaults...".yellow
68
-
69
- response = authenticated_request(
70
- :post,
71
- "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios",
72
- body: {}.to_json,
73
- headers: { 'Content-Type': 'application/json' }
74
- )
75
-
76
- if response.status.between?(200, 299)
77
- puts "BIOS reset to defaults successfully. Changes will be applied on next reboot.".green
78
- return true
79
- else
80
- raise Error, "Failed to reset BIOS: #{response.status} - #{response.body}"
81
- end
82
- end
8
+ # BIOS methods have been moved to the Bios module for better organization
9
+ # Use client.bios_attributes, client.set_bios_attribute, etc. instead
83
10
 
84
11
  def manager_network_protocol
85
12
  response = authenticated_request(:get, "/redfish/v1/Managers/1/NetworkProtocol")
@@ -61,6 +61,37 @@ module Supermicro
61
61
  sleep 1
62
62
  next
63
63
  end
64
+ elsif task_response.status == 404
65
+ # TaskMonitor endpoint not found - this is an error
66
+ spinner&.stop("Task endpoint not found", success: false)
67
+ debug "Task endpoint returned 404 - cannot monitor task", 1, :red
68
+ return { success: false, error: 'task_endpoint_not_found' }
69
+ elsif task_response.status == 400
70
+ # 400 can indicate task completion with error details in body
71
+ begin
72
+ if task_response.body && !task_response.body.empty?
73
+ task_info = JSON.parse(task_response.body)
74
+
75
+ # Check if this is a completed task with error
76
+ if task_info['TaskState'] == 'Exception'
77
+ spinner&.stop("Task failed: #{task_info['Message']}", success: false)
78
+ debug "✗ Task failed: #{task_info['Message']}", 1, :red
79
+ return { success: false, task: task_info, error: task_info['Message'] }
80
+ elsif task_info['TaskState'] == 'Completed'
81
+ spinner&.stop("Task completed", success: true)
82
+ debug "✓ Task completed", 2, :green
83
+ return { success: true, task: task_info }
84
+ else
85
+ # Unknown 400 response
86
+ debug "Unexpected 400 response with TaskState: #{task_info['TaskState']}", 2, :yellow
87
+ sleep 1
88
+ next
89
+ end
90
+ end
91
+ rescue JSON::ParserError
92
+ debug "400 response but couldn't parse body: #{task_response.body}", 1, :red
93
+ return { success: false, error: 'bad_request' }
94
+ end
64
95
  else
65
96
  debug "Unexpected task response: #{task_response.status}", 2, :yellow
66
97
  sleep 1
@@ -121,7 +152,14 @@ module Supermicro
121
152
  if !task_location && response.body && !response.body.empty?
122
153
  begin
123
154
  task_data = JSON.parse(response.body)
124
- task_location = task_data['@odata.id'] || task_data['TaskMonitor']
155
+ # Prefer @odata.id over TaskMonitor as TaskMonitor may return 404
156
+ task_location = task_data['@odata.id']
157
+
158
+ # Only use TaskMonitor if @odata.id is not available
159
+ if !task_location && task_data['TaskMonitor']
160
+ debug "Using TaskMonitor endpoint (may not be supported): #{task_data['TaskMonitor']}", 2, :yellow
161
+ task_location = task_data['TaskMonitor']
162
+ end
125
163
  rescue JSON::ParserError
126
164
  # No task info in body
127
165
  end
@@ -6,44 +6,80 @@ require 'colorize'
6
6
  module Supermicro
7
7
  module Utility
8
8
  def sel_log
9
- response = authenticated_request(:get, "/redfish/v1/Managers/1/LogServices/SEL/Entries?$expand=*($levels=1)")
9
+ # Supermicro uses Systems/1/LogServices/Log1/Entries for system health event logs
10
+ # Try Systems log first (health events)
11
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/LogServices/Log1/Entries?$expand=*($levels=1)")
10
12
 
11
13
  if response.status == 200
12
- begin
13
- data = JSON.parse(response.body)
14
-
15
- entries = data["Members"]&.map do |entry|
16
- {
17
- "id" => entry["Id"],
18
- "name" => entry["Name"],
19
- "created" => entry["Created"],
20
- "severity" => entry["Severity"],
21
- "message" => entry["Message"],
22
- "message_id" => entry["MessageId"],
23
- "sensor_type" => entry["SensorType"],
24
- "sensor_number" => entry["SensorNumber"]
25
- }
26
- end || []
27
-
28
- return entries.sort_by { |e| e["created"] || "" }.reverse
29
- rescue JSON::ParserError
30
- raise Error, "Failed to parse SEL log response: #{response.body}"
14
+ debug "Retrieved system health event logs", 2, :green
15
+ return parse_log_entries(response)
16
+ elsif response.status == 404
17
+ # Fallback to Manager logs (maintenance events)
18
+ debug "Systems log not found, trying Manager logs", 2, :yellow
19
+ response = authenticated_request(:get, "/redfish/v1/Managers/1/LogServices/Log1/Entries?$expand=*($levels=1)")
20
+
21
+ if response.status == 200
22
+ debug "Retrieved manager maintenance logs", 2, :green
23
+ return parse_log_entries(response)
24
+ elsif response.status == 404
25
+ debug "No log services available on this system", 1, :yellow
26
+ return []
31
27
  end
32
- else
33
- raise Error, "Failed to get SEL log. Status code: #{response.status}"
34
28
  end
29
+
30
+ if response.status != 200
31
+ debug "Failed to get system logs. Status code: #{response.status}", 1, :yellow
32
+ return []
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def parse_log_entries(response)
39
+ data = JSON.parse(response.body)
40
+
41
+ entries = data["Members"]&.map do |entry|
42
+ {
43
+ "id" => entry["Id"],
44
+ "name" => entry["Name"],
45
+ "created" => entry["Created"],
46
+ "severity" => entry["Severity"],
47
+ "message" => entry["Message"],
48
+ "message_id" => entry["MessageId"],
49
+ "sensor_type" => entry["SensorType"],
50
+ "sensor_number" => entry["SensorNumber"]
51
+ }
52
+ end || []
53
+
54
+ return entries.sort_by { |e| e["created"] || "" }.reverse
55
+ rescue JSON::ParserError
56
+ debug "Failed to parse log response", 1, :yellow
57
+ return []
35
58
  end
59
+
60
+ public
36
61
 
37
62
  def clear_sel_log
38
63
  puts "Clearing System Event Log...".yellow
39
64
 
65
+ # Try to clear System health logs first
40
66
  response = authenticated_request(
41
67
  :post,
42
- "/redfish/v1/Managers/1/LogServices/SEL/Actions/LogService.ClearLog",
68
+ "/redfish/v1/Systems/1/LogServices/Log1/Actions/LogService.ClearLog",
43
69
  body: {}.to_json,
44
70
  headers: { 'Content-Type': 'application/json' }
45
71
  )
46
72
 
73
+ if response.status == 404
74
+ # Fallback to Manager logs
75
+ response = authenticated_request(
76
+ :post,
77
+ "/redfish/v1/Managers/1/LogServices/Log1/Actions/LogService.ClearLog",
78
+ body: {}.to_json,
79
+ headers: { 'Content-Type': 'application/json' }
80
+ )
81
+ end
82
+
47
83
  if response.status.between?(200, 299)
48
84
  puts "SEL cleared successfully.".green
49
85
  return true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Supermicro
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
@@ -155,6 +155,7 @@ module Supermicro
155
155
 
156
156
  tries = 0
157
157
  max_tries = 3
158
+ last_error = nil
158
159
 
159
160
  while tries < max_tries
160
161
  begin
@@ -233,21 +234,26 @@ module Supermicro
233
234
  inserted_media = current_media.find { |m| m[:device] == device }
234
235
  if inserted_media && inserted_media[:image] == iso_url
235
236
  if inserted_media[:connected_via] == "NotConnected"
236
- debug "ERROR: Media mounted but NOT CONNECTED!", 1, :red
237
- debug "The ISO will NOT boot! ConnectedVia must be 'URI', not 'NotConnected'.", 1, :red
238
- return false
237
+ error_msg = "Media mounted but NOT CONNECTED! The ISO will NOT boot! ConnectedVia must be 'URI', not 'NotConnected'."
238
+ debug "ERROR: #{error_msg}", 1, :red
239
+ raise Error, error_msg
239
240
  else
240
241
  debug "Media mounted with status: #{inserted_media[:connected_via]}", 1, :green
241
242
  return true
242
243
  end
243
244
  else
244
- debug "Failed to verify media mount.", 1, :red
245
- return false
245
+ error_msg = "Failed to verify media mount - media not found on device #{device}"
246
+ debug error_msg, 1, :red
247
+ raise Error, error_msg
246
248
  end
247
249
  end
248
250
  else
249
251
  debug "Task failed or timed out", 1, :red
250
- return false
252
+ last_error = task_result[:error] || "Task failed with unknown error"
253
+ debug "Task error details: #{last_error}", 1, :red
254
+ tries += 1
255
+ sleep 2
256
+ next
251
257
  end
252
258
  elsif response.status.between?(200, 299)
253
259
  # Synchronous success (rare)
@@ -261,14 +267,16 @@ module Supermicro
261
267
  debug "✓ Media connected via URI", 1, :green
262
268
  return true
263
269
  elsif inserted_media[:connected_via] == "NotConnected"
264
- debug "WARNING: Media mounted but not connected!", 1, :red
265
- return false
270
+ error_msg = "Media mounted but not connected! ConnectedVia is 'NotConnected' - ISO will not boot"
271
+ debug "ERROR: #{error_msg}", 1, :red
272
+ raise Error, error_msg
266
273
  else
267
274
  debug "Media mounted with status: #{inserted_media[:connected_via]}", 1, :yellow
268
275
  return true
269
276
  end
270
277
  else
271
- debug "Could not verify media mount", 1, :yellow
278
+ # If we can't verify, still treat as success but log warning
279
+ debug "WARNING: Could not verify media mount status", 1, :yellow
272
280
  return true
273
281
  end
274
282
  elsif response.status == 400 && response.body.include?("already")
@@ -278,19 +286,54 @@ module Supermicro
278
286
  tries += 1
279
287
  else
280
288
  debug "Failed to insert media: #{response.status} - #{response.body}", 1, :red
289
+ last_error = "HTTP #{response.status}: #{response.body}"
281
290
  tries += 1
282
291
  sleep 2
283
292
  end
284
293
  rescue => e
285
294
  debug "Error inserting media: #{e.message}", 1, :red
295
+ last_error = e.message
286
296
  tries += 1
287
297
  sleep 2
288
298
  end
289
299
  end
290
300
 
291
- raise Error, "Failed to insert virtual media after #{max_tries} attempts"
301
+ error_msg = "Failed to insert virtual media after #{max_tries} attempts"
302
+ error_msg += ": #{last_error}" if last_error
303
+ raise Error, error_msg
292
304
  end
293
305
 
306
+ def test_iso_accessibility(iso_url)
307
+ debug "Testing if BMC can reach ISO URL: #{iso_url}", 1, :yellow
308
+
309
+ # Try to use Redfish's built-in validation if available
310
+ path = "/redfish/v1/Managers/1/VirtualMedia/test"
311
+ body = {
312
+ "Image" => iso_url,
313
+ "TestOnly" => true
314
+ }
315
+
316
+ begin
317
+ response = authenticated_request(
318
+ :post,
319
+ path,
320
+ body: body.to_json,
321
+ headers: { 'Content-Type': 'application/json' }
322
+ )
323
+
324
+ if response.status == 200
325
+ debug "✓ BMC can reach ISO URL", 1, :green
326
+ return true
327
+ else
328
+ debug "✗ BMC cannot reach ISO URL: #{response.body}", 1, :red
329
+ return false
330
+ end
331
+ rescue => e
332
+ debug "Cannot test ISO accessibility: #{e.message}", 2, :yellow
333
+ return nil
334
+ end
335
+ end
336
+
294
337
  def find_best_virtual_media_device
295
338
  media_list = virtual_media
296
339
 
data/lib/supermicro.rb CHANGED
@@ -49,6 +49,7 @@ require 'supermicro/system'
49
49
  require 'supermicro/tasks'
50
50
  require 'supermicro/virtual_media'
51
51
  require 'supermicro/boot'
52
+ require 'supermicro/bios'
52
53
  require 'supermicro/system_config'
53
54
  require 'supermicro/utility'
54
55
  require 'supermicro/license'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: supermicro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-29 00:00:00.000000000 Z
11
+ date: 2025-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -147,6 +147,7 @@ files:
147
147
  - LICENSE
148
148
  - README.md
149
149
  - lib/supermicro.rb
150
+ - lib/supermicro/bios.rb
150
151
  - lib/supermicro/boot.rb
151
152
  - lib/supermicro/client.rb
152
153
  - lib/supermicro/error.rb