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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/lib/supermicro/boot.rb +206 -0
- data/lib/supermicro/client.rb +420 -0
- data/lib/supermicro/error.rb +10 -0
- data/lib/supermicro/jobs.rb +179 -0
- data/lib/supermicro/license.rb +132 -0
- data/lib/supermicro/power.rb +169 -0
- data/lib/supermicro/session.rb +121 -0
- data/lib/supermicro/spinner.rb +179 -0
- data/lib/supermicro/storage.rb +180 -0
- data/lib/supermicro/system.rb +275 -0
- data/lib/supermicro/system_config.rb +201 -0
- data/lib/supermicro/tasks.rb +139 -0
- data/lib/supermicro/utility.rb +235 -0
- data/lib/supermicro/version.rb +5 -0
- data/lib/supermicro/virtual_media.rb +372 -0
- data/lib/supermicro.rb +55 -0
- data/supermicro.gemspec +41 -0
- metadata +193 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'spinner'
|
4
|
+
|
5
|
+
module Supermicro
|
6
|
+
module Tasks
|
7
|
+
include SpinnerHelper
|
8
|
+
|
9
|
+
def poll_task(task_location, timeout: 30, show_progress: true)
|
10
|
+
debug "Polling task: #{task_location}", 2
|
11
|
+
|
12
|
+
start_time = Time.now
|
13
|
+
last_percent = 0
|
14
|
+
task_name = task_location.split('/').last
|
15
|
+
|
16
|
+
# Use spinner if not in verbose mode
|
17
|
+
if show_progress && (!respond_to?(:verbosity) || verbosity == 0)
|
18
|
+
spinner = Spinner.new("Processing task #{task_name}", type: :dots, color: :cyan)
|
19
|
+
spinner.start
|
20
|
+
else
|
21
|
+
spinner = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
while (Time.now - start_time) < timeout
|
26
|
+
task_response = authenticated_request(:get, task_location)
|
27
|
+
|
28
|
+
# TaskMonitor returns 202 while running, then eventually returns the actual task
|
29
|
+
if task_response.status == 202
|
30
|
+
# Try to parse the 202 response for any status info
|
31
|
+
begin
|
32
|
+
if task_response.body && !task_response.body.empty?
|
33
|
+
monitor_data = JSON.parse(task_response.body)
|
34
|
+
if monitor_data['TaskState']
|
35
|
+
# We got task info even with 202
|
36
|
+
task_info = monitor_data
|
37
|
+
# Process it below
|
38
|
+
else
|
39
|
+
spinner&.update("Waiting for task to start...")
|
40
|
+
debug " Task pending (202 status)...", 3
|
41
|
+
sleep 1
|
42
|
+
next
|
43
|
+
end
|
44
|
+
else
|
45
|
+
spinner&.update("Task initializing...")
|
46
|
+
debug " Task still initializing...", 3
|
47
|
+
sleep 1
|
48
|
+
next
|
49
|
+
end
|
50
|
+
rescue JSON::ParserError
|
51
|
+
spinner&.update("Task running...")
|
52
|
+
debug " Task running (202)...", 3
|
53
|
+
sleep 1
|
54
|
+
next
|
55
|
+
end
|
56
|
+
elsif task_response.status == 200
|
57
|
+
begin
|
58
|
+
task_info = JSON.parse(task_response.body)
|
59
|
+
rescue JSON::ParserError => e
|
60
|
+
debug "Could not parse task response: #{e.message}", 2, :yellow
|
61
|
+
sleep 1
|
62
|
+
next
|
63
|
+
end
|
64
|
+
else
|
65
|
+
debug "Unexpected task response: #{task_response.status}", 2, :yellow
|
66
|
+
sleep 1
|
67
|
+
next
|
68
|
+
end
|
69
|
+
|
70
|
+
# Process task_info if we have it
|
71
|
+
if defined?(task_info) && task_info && task_info['TaskState']
|
72
|
+
percent = task_info['PercentComplete'] || 0
|
73
|
+
if percent > 0 && percent != last_percent
|
74
|
+
spinner&.update("Task progress: #{percent}%")
|
75
|
+
debug " Task progress: #{percent}%", 2
|
76
|
+
last_percent = percent
|
77
|
+
end
|
78
|
+
|
79
|
+
case task_info['TaskState']
|
80
|
+
when 'Completed'
|
81
|
+
spinner&.stop("Task completed successfully", success: true)
|
82
|
+
debug "✓ Task completed successfully", 2, :green
|
83
|
+
return { success: true, task: task_info }
|
84
|
+
when 'Exception', 'Killed', 'Cancelled'
|
85
|
+
spinner&.stop("Task failed: #{task_info['TaskState']}", success: false)
|
86
|
+
debug "✗ Task failed: #{task_info['TaskState']}", 1, :red
|
87
|
+
if task_info['Messages']
|
88
|
+
task_info['Messages'].each do |msg|
|
89
|
+
debug " #{msg['Message']}", 1, :red
|
90
|
+
end
|
91
|
+
end
|
92
|
+
return { success: false, task: task_info }
|
93
|
+
when 'Running', 'Starting', 'Pending', 'New'
|
94
|
+
# Still running
|
95
|
+
state_msg = task_info['TaskState'] == 'Running' ? "Running" : "Starting"
|
96
|
+
spinner&.update("Task #{state_msg.downcase}...")
|
97
|
+
debug " Task state: #{task_info['TaskState']}", 3
|
98
|
+
else
|
99
|
+
spinner&.update("Task state: #{task_info['TaskState']}")
|
100
|
+
debug " Unknown task state: #{task_info['TaskState']}", 2, :yellow
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
sleep 1
|
105
|
+
end
|
106
|
+
|
107
|
+
spinner&.stop("Task timed out after #{timeout} seconds", success: false)
|
108
|
+
debug "Task polling timed out after #{timeout} seconds", 1, :yellow
|
109
|
+
{ success: false, error: 'timeout' }
|
110
|
+
ensure
|
111
|
+
spinner&.stop if spinner
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def wait_for_task_completion(response, timeout: 30)
|
116
|
+
return { success: true } unless response.status == 202
|
117
|
+
|
118
|
+
# Get task location from response
|
119
|
+
task_location = response.headers['Location'] || response.headers['location']
|
120
|
+
|
121
|
+
if !task_location && response.body && !response.body.empty?
|
122
|
+
begin
|
123
|
+
task_data = JSON.parse(response.body)
|
124
|
+
task_location = task_data['@odata.id'] || task_data['TaskMonitor']
|
125
|
+
rescue JSON::ParserError
|
126
|
+
# No task info in body
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if task_location
|
131
|
+
debug "Task started: #{task_location}", 2
|
132
|
+
poll_task(task_location, timeout: timeout)
|
133
|
+
else
|
134
|
+
debug "No task location found, assuming synchronous completion", 2
|
135
|
+
{ success: true }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'colorize'
|
5
|
+
|
6
|
+
module Supermicro
|
7
|
+
module Utility
|
8
|
+
def sel_log
|
9
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/1/LogServices/SEL/Entries?$expand=*($levels=1)")
|
10
|
+
|
11
|
+
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}"
|
31
|
+
end
|
32
|
+
else
|
33
|
+
raise Error, "Failed to get SEL log. Status code: #{response.status}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear_sel_log
|
38
|
+
puts "Clearing System Event Log...".yellow
|
39
|
+
|
40
|
+
response = authenticated_request(
|
41
|
+
:post,
|
42
|
+
"/redfish/v1/Managers/1/LogServices/SEL/Actions/LogService.ClearLog",
|
43
|
+
body: {}.to_json,
|
44
|
+
headers: { 'Content-Type': 'application/json' }
|
45
|
+
)
|
46
|
+
|
47
|
+
if response.status.between?(200, 299)
|
48
|
+
puts "SEL cleared successfully.".green
|
49
|
+
return true
|
50
|
+
else
|
51
|
+
raise Error, "Failed to clear SEL: #{response.status} - #{response.body}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def sel_summary(limit: 10)
|
56
|
+
puts "\n=== System Event Log ===".green
|
57
|
+
|
58
|
+
entries = sel_log
|
59
|
+
|
60
|
+
if entries.empty?
|
61
|
+
puts "No log entries found.".yellow
|
62
|
+
return entries
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "Total entries: #{entries.length}".cyan
|
66
|
+
puts "\nMost recent #{limit} entries:".cyan
|
67
|
+
|
68
|
+
entries.first(limit).each do |entry|
|
69
|
+
severity_color = case entry["severity"]
|
70
|
+
when "Critical" then :red
|
71
|
+
when "Warning" then :yellow
|
72
|
+
when "OK" then :green
|
73
|
+
else :white
|
74
|
+
end
|
75
|
+
|
76
|
+
puts "\n[#{entry['created']}] #{entry['severity']}".send(severity_color)
|
77
|
+
puts " #{entry['message']}"
|
78
|
+
puts " ID: #{entry['id']} | MessageID: #{entry['message_id']}" if entry['message_id']
|
79
|
+
end
|
80
|
+
|
81
|
+
entries
|
82
|
+
end
|
83
|
+
|
84
|
+
def accounts
|
85
|
+
response = authenticated_request(:get, "/redfish/v1/AccountService/Accounts?$expand=*($levels=1)")
|
86
|
+
|
87
|
+
if response.status == 200
|
88
|
+
begin
|
89
|
+
data = JSON.parse(response.body)
|
90
|
+
|
91
|
+
accounts = data["Members"]&.map do |account|
|
92
|
+
{
|
93
|
+
"id" => account["Id"],
|
94
|
+
"username" => account["UserName"],
|
95
|
+
"enabled" => account["Enabled"],
|
96
|
+
"locked" => account["Locked"],
|
97
|
+
"role_id" => account["RoleId"],
|
98
|
+
"description" => account["Description"]
|
99
|
+
}
|
100
|
+
end || []
|
101
|
+
|
102
|
+
return accounts
|
103
|
+
rescue JSON::ParserError
|
104
|
+
raise Error, "Failed to parse accounts response: #{response.body}"
|
105
|
+
end
|
106
|
+
else
|
107
|
+
raise Error, "Failed to get accounts. Status code: #{response.status}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def create_account(username:, password:, role: "Administrator")
|
112
|
+
puts "Creating account #{username} with role #{role}...".yellow
|
113
|
+
|
114
|
+
body = {
|
115
|
+
"UserName" => username,
|
116
|
+
"Password" => password,
|
117
|
+
"RoleId" => role,
|
118
|
+
"Enabled" => true
|
119
|
+
}
|
120
|
+
|
121
|
+
response = authenticated_request(
|
122
|
+
:post,
|
123
|
+
"/redfish/v1/AccountService/Accounts",
|
124
|
+
body: body.to_json,
|
125
|
+
headers: { 'Content-Type': 'application/json' }
|
126
|
+
)
|
127
|
+
|
128
|
+
if response.status.between?(200, 299)
|
129
|
+
puts "Account created successfully.".green
|
130
|
+
return true
|
131
|
+
else
|
132
|
+
raise Error, "Failed to create account: #{response.status} - #{response.body}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def delete_account(username)
|
137
|
+
accounts_list = accounts
|
138
|
+
account = accounts_list.find { |a| a["username"] == username }
|
139
|
+
|
140
|
+
unless account
|
141
|
+
raise Error, "Account #{username} not found"
|
142
|
+
end
|
143
|
+
|
144
|
+
puts "Deleting account #{username}...".yellow
|
145
|
+
|
146
|
+
response = authenticated_request(
|
147
|
+
:delete,
|
148
|
+
"/redfish/v1/AccountService/Accounts/#{account["id"]}"
|
149
|
+
)
|
150
|
+
|
151
|
+
if response.status.between?(200, 299)
|
152
|
+
puts "Account deleted successfully.".green
|
153
|
+
return true
|
154
|
+
else
|
155
|
+
raise Error, "Failed to delete account: #{response.status} - #{response.body}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def update_account_password(username:, new_password:)
|
160
|
+
accounts_list = accounts
|
161
|
+
account = accounts_list.find { |a| a["username"] == username }
|
162
|
+
|
163
|
+
unless account
|
164
|
+
raise Error, "Account #{username} not found"
|
165
|
+
end
|
166
|
+
|
167
|
+
puts "Updating password for account #{username}...".yellow
|
168
|
+
|
169
|
+
body = {
|
170
|
+
"Password" => new_password
|
171
|
+
}
|
172
|
+
|
173
|
+
response = authenticated_request(
|
174
|
+
:patch,
|
175
|
+
"/redfish/v1/AccountService/Accounts/#{account["id"]}",
|
176
|
+
body: body.to_json,
|
177
|
+
headers: { 'Content-Type': 'application/json' }
|
178
|
+
)
|
179
|
+
|
180
|
+
if response.status.between?(200, 299)
|
181
|
+
puts "Password updated successfully.".green
|
182
|
+
return true
|
183
|
+
else
|
184
|
+
raise Error, "Failed to update password: #{response.status} - #{response.body}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def sessions
|
189
|
+
response = authenticated_request(:get, "/redfish/v1/SessionService/Sessions?$expand=*($levels=1)")
|
190
|
+
|
191
|
+
if response.status == 200
|
192
|
+
begin
|
193
|
+
data = JSON.parse(response.body)
|
194
|
+
|
195
|
+
sessions = data["Members"]&.map do |session|
|
196
|
+
{
|
197
|
+
"id" => session["Id"],
|
198
|
+
"username" => session["UserName"],
|
199
|
+
"created_time" => session["CreatedTime"],
|
200
|
+
"client_ip" => session.dig("Oem", "Supermicro", "ClientIP") || session["ClientOriginIPAddress"]
|
201
|
+
}
|
202
|
+
end || []
|
203
|
+
|
204
|
+
return sessions
|
205
|
+
rescue JSON::ParserError
|
206
|
+
raise Error, "Failed to parse sessions response: #{response.body}"
|
207
|
+
end
|
208
|
+
else
|
209
|
+
raise Error, "Failed to get sessions. Status code: #{response.status}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def service_info
|
214
|
+
response = authenticated_request(:get, "/redfish/v1")
|
215
|
+
|
216
|
+
if response.status == 200
|
217
|
+
begin
|
218
|
+
data = JSON.parse(response.body)
|
219
|
+
|
220
|
+
{
|
221
|
+
"service_version" => data["RedfishVersion"],
|
222
|
+
"uuid" => data["UUID"],
|
223
|
+
"product" => data["Product"],
|
224
|
+
"vendor" => data["Vendor"],
|
225
|
+
"oem" => data["Oem"]
|
226
|
+
}
|
227
|
+
rescue JSON::ParserError
|
228
|
+
raise Error, "Failed to parse service info response: #{response.body}"
|
229
|
+
end
|
230
|
+
else
|
231
|
+
raise Error, "Failed to get service info. Status code: #{response.status}"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|