supermicro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'colorize'
5
+
6
+ module Supermicro
7
+ module Storage
8
+ def storage_controllers
9
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/Storage?$expand=*($levels=1)")
10
+
11
+ if response.status == 200
12
+ begin
13
+ data = JSON.parse(response.body)
14
+
15
+ controllers = data["Members"].map do |controller|
16
+ {
17
+ "id" => controller["Id"],
18
+ "name" => controller["Name"],
19
+ "status" => controller.dig("Status", "Health") || "N/A",
20
+ "storage_controllers" => controller["StorageControllers"]&.map { |sc|
21
+ {
22
+ "name" => sc["Name"],
23
+ "manufacturer" => sc["Manufacturer"],
24
+ "model" => sc["Model"],
25
+ "firmware_version" => sc["FirmwareVersion"],
26
+ "speed_gbps" => sc["SpeedGbps"],
27
+ "supported_protocols" => sc["SupportedControllerProtocols"]
28
+ }
29
+ }
30
+ }
31
+ end
32
+
33
+ return controllers
34
+ rescue JSON::ParserError
35
+ raise Error, "Failed to parse storage controllers response: #{response.body}"
36
+ end
37
+ else
38
+ raise Error, "Failed to get storage controllers. Status code: #{response.status}"
39
+ end
40
+ end
41
+
42
+ def drives
43
+ all_drives = []
44
+
45
+ controllers = storage_controllers
46
+
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)
60
+
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
81
+ end
82
+ end
83
+ rescue JSON::ParserError
84
+ debug "Failed to parse storage data for controller #{controller_id}", 1, :yellow
85
+ end
86
+ end
87
+ end
88
+
89
+ return all_drives
90
+ end
91
+
92
+ def volumes
93
+ all_volumes = []
94
+
95
+ controllers = storage_controllers
96
+
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
+ }
120
+ end
121
+ end
122
+ rescue JSON::ParserError
123
+ debug "Failed to parse volumes data for controller #{controller_id}", 1, :yellow
124
+ end
125
+ end
126
+ end
127
+
128
+ return all_volumes
129
+ end
130
+
131
+ def storage_summary
132
+ puts "\n=== Storage Summary ===".green
133
+
134
+ controllers = storage_controllers
135
+ puts "\nStorage Controllers:".cyan
136
+ controllers.each do |controller|
137
+ puts " #{controller['name']} (#{controller['id']})".yellow
138
+ if controller["storage_controllers"]
139
+ controller["storage_controllers"].each do |sc|
140
+ puts " - #{sc['manufacturer']} #{sc['model']}".light_cyan
141
+ puts " Firmware: #{sc['firmware_version']}" if sc['firmware_version']
142
+ puts " Speed: #{sc['speed_gbps']} Gbps" if sc['speed_gbps']
143
+ end
144
+ end
145
+ end
146
+
147
+ all_drives = drives
148
+ puts "\nPhysical Drives:".cyan
149
+ all_drives.each do |drive|
150
+ puts " #{drive['name']} (#{drive['id']})".yellow
151
+ puts " - #{drive['manufacturer']} #{drive['model']}".light_cyan
152
+ puts " Capacity: #{drive['capacity_gb']} GB"
153
+ puts " Type: #{drive['media_type']} / #{drive['protocol']}"
154
+ puts " Status: #{drive['status']}"
155
+ puts " Serial: #{drive['serial']}"
156
+ if drive['predicted_media_life_left_percent']
157
+ puts " Life Remaining: #{drive['predicted_media_life_left_percent']}%"
158
+ end
159
+ end
160
+
161
+ all_volumes = volumes
162
+ if all_volumes.any?
163
+ puts "\nVolumes:".cyan
164
+ all_volumes.each do |volume|
165
+ puts " #{volume['name']} (#{volume['id']})".yellow
166
+ puts " - Capacity: #{volume['capacity_gb']} GB"
167
+ puts " Type: #{volume['volume_type']} / #{volume['raid_type']}"
168
+ puts " Status: #{volume['status']}"
169
+ puts " Encrypted: #{volume['encrypted']}"
170
+ end
171
+ end
172
+
173
+ {
174
+ "controllers" => controllers,
175
+ "drives" => all_drives,
176
+ "volumes" => all_volumes
177
+ }
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'colorize'
5
+
6
+ module Supermicro
7
+ module System
8
+ def memory
9
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/Memory")
10
+
11
+ if response.status == 200
12
+ begin
13
+ data = JSON.parse(response.body)
14
+
15
+ memory = data["Members"].map do |m|
16
+ next if m["CapacityMiB"].nil? || m["CapacityMiB"] == 0
17
+
18
+ dimm_name = m["DeviceLocator"] || m["Name"]
19
+ bank_match = /([A-Z])(\d+)/.match(dimm_name)
20
+
21
+ if bank_match
22
+ bank, index = bank_match.captures
23
+ else
24
+ bank = dimm_name
25
+ index = 0
26
+ end
27
+
28
+ {
29
+ "model" => m["Manufacturer"],
30
+ "name" => dimm_name,
31
+ "capacity_bytes" => m["CapacityMiB"].to_i * 1024 * 1024,
32
+ "health" => m.dig("Status", "Health") || "N/A",
33
+ "speed_mhz" => m["OperatingSpeedMhz"],
34
+ "part_number" => m["PartNumber"],
35
+ "serial" => m["SerialNumber"],
36
+ "bank" => bank,
37
+ "index" => index.to_i,
38
+ "memory_type" => m["MemoryDeviceType"]
39
+ }
40
+ end.compact
41
+
42
+ return memory.sort_by { |m| [m["bank"] || "Z", m["index"] || 999] }
43
+ rescue JSON::ParserError
44
+ raise Error, "Failed to parse memory response: #{response.body}"
45
+ end
46
+ else
47
+ raise Error, "Failed to get memory. Status code: #{response.status}"
48
+ end
49
+ end
50
+
51
+ def psus
52
+ response = authenticated_request(:get, "/redfish/v1/Chassis/1/Power")
53
+
54
+ if response.status == 200
55
+ begin
56
+ data = JSON.parse(response.body)
57
+ psus = data["PowerSupplies"].map do |psu|
58
+ health = psu.dig("Status", "Health") || "Unknown"
59
+ watts = psu["PowerInputWatts"] || psu["LastPowerOutputWatts"] || 0
60
+
61
+ {
62
+ "name" => psu["Name"],
63
+ "voltage" => psu["LineInputVoltage"],
64
+ "voltage_human" => psu["LineInputVoltageType"],
65
+ "watts" => watts,
66
+ "part" => psu["PartNumber"],
67
+ "model" => psu["Model"],
68
+ "serial" => psu["SerialNumber"],
69
+ "status" => health,
70
+ "manufacturer" => psu["Manufacturer"]
71
+ }
72
+ end
73
+
74
+ return psus
75
+ rescue JSON::ParserError
76
+ raise Error, "Failed to parse PSU response: #{response.body}"
77
+ end
78
+ else
79
+ raise Error, "Failed to get PSUs. Status code: #{response.status}"
80
+ end
81
+ end
82
+
83
+ def fans
84
+ tries = 0
85
+ max_tries = 3
86
+
87
+ while tries < max_tries
88
+ begin
89
+ response = authenticated_request(:get, "/redfish/v1/Chassis/1/Thermal?$expand=*($levels=1)")
90
+
91
+ if response.status == 200
92
+ data = JSON.parse(response.body)
93
+
94
+ fans = data["Fans"].map do |fan|
95
+ health = fan.dig("Status", "Health") || "Unknown"
96
+ rpm = fan["Reading"] || fan["CurrentReading"] || 0
97
+
98
+ {
99
+ "name" => fan["Name"],
100
+ "rpm" => rpm,
101
+ "status" => health,
102
+ "min_rpm" => fan["MinReadingRange"],
103
+ "max_rpm" => fan["MaxReadingRange"]
104
+ }
105
+ end
106
+
107
+ return fans
108
+ elsif response.status.between?(400, 499)
109
+ power_response = authenticated_request(:get, "/redfish/v1/Systems/1?$select=PowerState")
110
+ if power_response.status == 200 && JSON.parse(power_response.body)["PowerState"] == "Off"
111
+ puts "WARN: System is off. Fans are not available.".yellow
112
+ return []
113
+ else
114
+ raise Error, "Failed to get fans: #{response.status}"
115
+ end
116
+ else
117
+ raise Error, "Failed to get fans: #{response.status}"
118
+ end
119
+ rescue => e
120
+ tries += 1
121
+ if tries >= max_tries
122
+ raise Error, "Failed to get fans after #{max_tries} attempts: #{e.message}"
123
+ end
124
+ sleep 2
125
+ end
126
+ end
127
+ end
128
+
129
+ def cpus
130
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/Processors")
131
+
132
+ if response.status == 200
133
+ begin
134
+ data = JSON.parse(response.body)
135
+
136
+ cpus = data["Members"].map do |member|
137
+ # Fetch each processor individually
138
+ cpu_path = member["@odata.id"]
139
+ cpu_response = authenticated_request(:get, cpu_path)
140
+
141
+ if cpu_response.status == 200
142
+ cpu = JSON.parse(cpu_response.body)
143
+
144
+ next unless cpu["ProcessorType"] == "CPU"
145
+
146
+ {
147
+ "socket" => cpu["Socket"] || cpu["Id"],
148
+ "model" => cpu["Model"],
149
+ "manufacturer" => cpu["Manufacturer"],
150
+ "cores" => cpu["TotalCores"],
151
+ "threads" => cpu["TotalThreads"],
152
+ "speed_mhz" => cpu["MaxSpeedMHz"],
153
+ "health" => cpu.dig("Status", "Health") || "N/A"
154
+ }
155
+ else
156
+ nil
157
+ end
158
+ end.compact
159
+
160
+ return cpus
161
+ rescue JSON::ParserError => e
162
+ raise Error, "Failed to parse CPU response: #{e.message}"
163
+ end
164
+ else
165
+ raise Error, "Failed to get CPUs. Status code: #{response.status}"
166
+ end
167
+ end
168
+
169
+ def nics
170
+ response = authenticated_request(:get, "/redfish/v1/Systems/1/EthernetInterfaces?$expand=*($levels=1)")
171
+
172
+ if response.status == 200
173
+ begin
174
+ data = JSON.parse(response.body)
175
+
176
+ nics = data["Members"].map do |nic|
177
+ {
178
+ "id" => nic["Id"],
179
+ "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")
186
+ }
187
+ end
188
+
189
+ return nics
190
+ rescue JSON::ParserError
191
+ raise Error, "Failed to parse NIC response: #{response.body}"
192
+ end
193
+ else
194
+ raise Error, "Failed to get NICs. Status code: #{response.status}"
195
+ end
196
+ end
197
+
198
+ def system_info
199
+ response = authenticated_request(:get, "/redfish/v1/Systems/1")
200
+
201
+ if response.status == 200
202
+ begin
203
+ data = JSON.parse(response.body)
204
+
205
+ {
206
+ "name" => data["Name"],
207
+ "model" => data["Model"],
208
+ "manufacturer" => data["Manufacturer"],
209
+ "serial" => data["SerialNumber"],
210
+ "uuid" => data["UUID"],
211
+ "bios_version" => data["BiosVersion"],
212
+ "power_state" => data["PowerState"],
213
+ "health" => data.dig("Status", "Health"),
214
+ "total_memory_gb" => data["MemorySummary"]["TotalSystemMemoryGiB"],
215
+ "processor_count" => data.dig("ProcessorSummary", "Count"),
216
+ "processor_model" => data.dig("ProcessorSummary", "Model")
217
+ }
218
+ rescue JSON::ParserError
219
+ raise Error, "Failed to parse system info response: #{response.body}"
220
+ end
221
+ else
222
+ raise Error, "Failed to get system info. Status code: #{response.status}"
223
+ end
224
+ end
225
+
226
+ def power_consumption
227
+ response = authenticated_request(:get, "/redfish/v1/Chassis/1/Power")
228
+
229
+ if response.status == 200
230
+ begin
231
+ data = JSON.parse(response.body)
232
+
233
+ power_control = data["PowerControl"].first if data["PowerControl"]
234
+
235
+ {
236
+ "consumed_watts" => power_control["PowerConsumedWatts"],
237
+ "capacity_watts" => power_control["PowerCapacityWatts"],
238
+ "average_watts" => power_control["PowerAverage"],
239
+ "min_watts" => power_control["MinConsumedWatts"],
240
+ "max_watts" => power_control["MaxConsumedWatts"]
241
+ }
242
+ rescue JSON::ParserError
243
+ raise Error, "Failed to parse power consumption response: #{response.body}"
244
+ end
245
+ else
246
+ raise Error, "Failed to get power consumption. Status code: #{response.status}"
247
+ end
248
+ end
249
+
250
+ def temperatures
251
+ response = authenticated_request(:get, "/redfish/v1/Chassis/1/Thermal")
252
+
253
+ if response.status == 200
254
+ begin
255
+ data = JSON.parse(response.body)
256
+
257
+ temps = data["Temperatures"].map do |temp|
258
+ {
259
+ "name" => temp["Name"],
260
+ "reading_celsius" => temp["ReadingCelsius"],
261
+ "upper_threshold" => temp["UpperThresholdCritical"],
262
+ "status" => temp.dig("Status", "Health") || "N/A"
263
+ }
264
+ end
265
+
266
+ return temps
267
+ rescue JSON::ParserError
268
+ raise Error, "Failed to parse temperature response: #{response.body}"
269
+ end
270
+ else
271
+ raise Error, "Failed to get temperatures. Status code: #{response.status}"
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'colorize'
5
+
6
+ module Supermicro
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
83
+
84
+ def manager_network_protocol
85
+ response = authenticated_request(:get, "/redfish/v1/Managers/1/NetworkProtocol")
86
+
87
+ if response.status == 200
88
+ begin
89
+ data = JSON.parse(response.body)
90
+
91
+ protocols = {}
92
+ ["HTTP", "HTTPS", "IPMI", "SSH", "SNMP", "VirtualMedia", "KVMIP", "NTP", "Telnet"].each do |proto|
93
+ if data[proto]
94
+ protocols[proto.downcase] = {
95
+ "enabled" => data[proto]["ProtocolEnabled"],
96
+ "port" => data[proto]["Port"]
97
+ }
98
+ end
99
+ end
100
+
101
+ protocols
102
+ rescue JSON::ParserError
103
+ raise Error, "Failed to parse network protocol response: #{response.body}"
104
+ end
105
+ else
106
+ raise Error, "Failed to get network protocols. Status code: #{response.status}"
107
+ end
108
+ end
109
+
110
+ def set_network_protocol(protocol, enabled:, port: nil)
111
+ puts "Configuring #{protocol} protocol...".yellow
112
+
113
+ proto_key = protocol.upcase
114
+ body = {
115
+ proto_key => {
116
+ "ProtocolEnabled" => enabled
117
+ }
118
+ }
119
+
120
+ body[proto_key]["Port"] = port if port
121
+
122
+ response = authenticated_request(
123
+ :patch,
124
+ "/redfish/v1/Managers/1/NetworkProtocol",
125
+ body: body.to_json,
126
+ headers: { 'Content-Type': 'application/json' }
127
+ )
128
+
129
+ if response.status.between?(200, 299)
130
+ puts "Network protocol configured successfully.".green
131
+ return true
132
+ else
133
+ raise Error, "Failed to configure network protocol: #{response.status} - #{response.body}"
134
+ end
135
+ end
136
+
137
+ def manager_info
138
+ response = authenticated_request(:get, "/redfish/v1/Managers/1")
139
+
140
+ if response.status == 200
141
+ begin
142
+ data = JSON.parse(response.body)
143
+
144
+ {
145
+ "name" => data["Name"],
146
+ "model" => data["Model"],
147
+ "firmware_version" => data["FirmwareVersion"],
148
+ "uuid" => data["UUID"],
149
+ "status" => data.dig("Status", "Health"),
150
+ "datetime" => data["DateTime"],
151
+ "datetime_local_offset" => data["DateTimeLocalOffset"]
152
+ }
153
+ rescue JSON::ParserError
154
+ raise Error, "Failed to parse manager info response: #{response.body}"
155
+ end
156
+ else
157
+ raise Error, "Failed to get manager info. Status code: #{response.status}"
158
+ end
159
+ end
160
+
161
+ def set_manager_datetime(datetime_str)
162
+ puts "Setting BMC datetime to #{datetime_str}...".yellow
163
+
164
+ body = {
165
+ "DateTime" => datetime_str
166
+ }
167
+
168
+ response = authenticated_request(
169
+ :patch,
170
+ "/redfish/v1/Managers/1",
171
+ body: body.to_json,
172
+ headers: { 'Content-Type': 'application/json' }
173
+ )
174
+
175
+ if response.status.between?(200, 299)
176
+ puts "DateTime set successfully.".green
177
+ return true
178
+ else
179
+ raise Error, "Failed to set datetime: #{response.status} - #{response.body}"
180
+ end
181
+ end
182
+
183
+ def reset_manager
184
+ puts "Resetting BMC...".yellow
185
+
186
+ response = authenticated_request(
187
+ :post,
188
+ "/redfish/v1/Managers/1/Actions/Manager.Reset",
189
+ body: { "ResetType": "GracefulRestart" }.to_json,
190
+ headers: { 'Content-Type': 'application/json' }
191
+ )
192
+
193
+ if response.status.between?(200, 299)
194
+ puts "BMC reset initiated successfully.".green
195
+ return true
196
+ else
197
+ raise Error, "Failed to reset BMC: #{response.status} - #{response.body}"
198
+ end
199
+ end
200
+ end
201
+ end