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,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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supermicro
4
+ VERSION = "0.1.0"
5
+ end