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,420 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday/multipart'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'base64'
|
7
|
+
require 'uri'
|
8
|
+
require 'httparty'
|
9
|
+
require 'json'
|
10
|
+
require 'colorize'
|
11
|
+
|
12
|
+
module Supermicro
|
13
|
+
class Client
|
14
|
+
attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :session, :host_header
|
15
|
+
attr_accessor :direct_mode, :verbosity, :retry_count, :retry_delay
|
16
|
+
|
17
|
+
include Power
|
18
|
+
include Debuggable
|
19
|
+
include Jobs
|
20
|
+
include Storage
|
21
|
+
include System
|
22
|
+
include VirtualMedia
|
23
|
+
include Boot
|
24
|
+
include SystemConfig
|
25
|
+
include Utility
|
26
|
+
include License
|
27
|
+
include Tasks
|
28
|
+
|
29
|
+
def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, retry_count: 3, retry_delay: 1, host_header: nil)
|
30
|
+
@host = host
|
31
|
+
@username = username
|
32
|
+
@password = password
|
33
|
+
@port = port
|
34
|
+
@use_ssl = use_ssl
|
35
|
+
@verify_ssl = verify_ssl
|
36
|
+
@direct_mode = direct_mode
|
37
|
+
@host_header = host_header
|
38
|
+
@verbosity = 0
|
39
|
+
@retry_count = retry_count
|
40
|
+
@retry_delay = retry_delay
|
41
|
+
|
42
|
+
@session = Session.new(self)
|
43
|
+
|
44
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@session))
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.finalizer(session)
|
48
|
+
proc do
|
49
|
+
begin
|
50
|
+
session.delete if session.x_auth_token
|
51
|
+
rescue
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.connect(host:, username:, password:, **options)
|
57
|
+
client = new(host: host, username: username, password: password, **options)
|
58
|
+
return client unless block_given?
|
59
|
+
|
60
|
+
begin
|
61
|
+
client.login
|
62
|
+
yield client
|
63
|
+
ensure
|
64
|
+
client.logout
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def connection
|
69
|
+
@connection ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday|
|
70
|
+
faraday.request :multipart
|
71
|
+
faraday.request :url_encoded
|
72
|
+
faraday.adapter Faraday.default_adapter
|
73
|
+
if @verbosity > 0
|
74
|
+
faraday.response :logger, Logger.new(STDOUT), bodies: @verbosity >= 2 do |logger|
|
75
|
+
logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
|
76
|
+
logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def login
|
83
|
+
if @direct_mode
|
84
|
+
debug "Using direct mode (Basic Auth) for all requests", 1, :light_yellow
|
85
|
+
return true
|
86
|
+
end
|
87
|
+
|
88
|
+
if session.create
|
89
|
+
debug "Successfully logged in to Supermicro BMC using Redfish session", 1, :green
|
90
|
+
return true
|
91
|
+
else
|
92
|
+
debug "Failed to create Redfish session, falling back to direct mode", 1, :light_yellow
|
93
|
+
@direct_mode = true
|
94
|
+
return true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def logout
|
99
|
+
session.delete if session.x_auth_token
|
100
|
+
debug "Logged out from Supermicro BMC", 1, :green
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
|
104
|
+
def authenticated_request(method, path, body: nil, headers: {}, timeout: nil, open_timeout: nil, **options)
|
105
|
+
request_options = {
|
106
|
+
body: body,
|
107
|
+
headers: headers,
|
108
|
+
timeout: timeout,
|
109
|
+
open_timeout: open_timeout
|
110
|
+
}.merge(options).compact
|
111
|
+
|
112
|
+
with_retries do
|
113
|
+
_perform_authenticated_request(method, path, request_options)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def get(path:, headers: {})
|
118
|
+
with_retries do
|
119
|
+
_perform_get(path: path, headers: headers)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def base_url
|
124
|
+
protocol = use_ssl ? 'https' : 'http'
|
125
|
+
"#{protocol}://#{host}:#{port}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def redfish_version
|
129
|
+
response = authenticated_request(:get, "/redfish/v1")
|
130
|
+
if response.status == 200
|
131
|
+
data = JSON.parse(response.body)
|
132
|
+
data["RedfishVersion"]
|
133
|
+
else
|
134
|
+
raise Error, "Failed to get Redfish version: #{response.status} - #{response.body}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_firmware_version
|
139
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/1?$select=FirmwareVersion")
|
140
|
+
|
141
|
+
if response.status == 200
|
142
|
+
begin
|
143
|
+
data = JSON.parse(response.body)
|
144
|
+
return data["FirmwareVersion"]
|
145
|
+
rescue JSON::ParserError
|
146
|
+
raise Error, "Failed to parse firmware version response: #{response.body}"
|
147
|
+
end
|
148
|
+
else
|
149
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/1")
|
150
|
+
|
151
|
+
if response.status == 200
|
152
|
+
begin
|
153
|
+
data = JSON.parse(response.body)
|
154
|
+
return data["FirmwareVersion"]
|
155
|
+
rescue JSON::ParserError
|
156
|
+
raise Error, "Failed to parse firmware version response: #{response.body}"
|
157
|
+
end
|
158
|
+
else
|
159
|
+
raise Error, "Failed to get firmware version. Status code: #{response.status}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def with_retries(max_retries = nil, initial_delay = nil, error_classes = nil)
|
165
|
+
max_retries ||= @retry_count
|
166
|
+
initial_delay ||= @retry_delay
|
167
|
+
error_classes ||= [StandardError]
|
168
|
+
|
169
|
+
retries = 0
|
170
|
+
begin
|
171
|
+
yield
|
172
|
+
rescue *error_classes => e
|
173
|
+
retries += 1
|
174
|
+
if retries <= max_retries
|
175
|
+
delay = initial_delay * (retries ** 1.5).to_i
|
176
|
+
debug "RETRY: #{e.message} - Attempt #{retries}/#{max_retries}, waiting #{delay}s", 1, :yellow
|
177
|
+
sleep delay
|
178
|
+
retry
|
179
|
+
else
|
180
|
+
debug "MAX RETRIES REACHED: #{e.message} after #{max_retries} attempts", 1, :red
|
181
|
+
raise e
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def wait_for_task(task_id)
|
187
|
+
task = nil
|
188
|
+
|
189
|
+
begin
|
190
|
+
loop do
|
191
|
+
task_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{task_id}")
|
192
|
+
|
193
|
+
case task_response.status
|
194
|
+
when 200..299
|
195
|
+
task = JSON.parse(task_response.body)
|
196
|
+
|
197
|
+
if task["TaskState"] != "Running"
|
198
|
+
break
|
199
|
+
end
|
200
|
+
|
201
|
+
percent_complete = nil
|
202
|
+
if task["Oem"] && task["Oem"]["Supermicro"] && task["Oem"]["Supermicro"]["PercentComplete"]
|
203
|
+
percent_complete = task["Oem"]["Supermicro"]["PercentComplete"]
|
204
|
+
debug "Task progress: #{percent_complete}% complete", 1
|
205
|
+
end
|
206
|
+
|
207
|
+
debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
|
208
|
+
sleep 5
|
209
|
+
else
|
210
|
+
return {
|
211
|
+
status: :failed,
|
212
|
+
error: "Failed to check task status: #{task_response.status} - #{task_response.body}"
|
213
|
+
}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
if task["TaskState"] == "Completed" && task["TaskStatus"] == "OK"
|
218
|
+
return { status: :success }
|
219
|
+
else
|
220
|
+
debug task.inspect, 1, :yellow
|
221
|
+
|
222
|
+
messages = []
|
223
|
+
if task["Messages"] && task["Messages"].is_a?(Array)
|
224
|
+
messages = task["Messages"].map { |m| m["Message"] }.compact
|
225
|
+
end
|
226
|
+
|
227
|
+
return {
|
228
|
+
status: :failed,
|
229
|
+
task_state: task["TaskState"],
|
230
|
+
task_status: task["TaskStatus"],
|
231
|
+
messages: messages,
|
232
|
+
error: messages.first || "Task failed with state: #{task["TaskState"]}"
|
233
|
+
}
|
234
|
+
end
|
235
|
+
rescue => e
|
236
|
+
return { status: :error, error: "Exception monitoring task: #{e.message}" }
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def handle_response(response)
|
241
|
+
if response.headers["location"]
|
242
|
+
return handle_location(response.headers["location"])
|
243
|
+
end
|
244
|
+
|
245
|
+
if response.status.between?(200, 299)
|
246
|
+
return response.body
|
247
|
+
else
|
248
|
+
raise Error, "Failed to #{response.status} - #{response.body}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def handle_location(location)
|
253
|
+
return nil if location.nil? || location.empty?
|
254
|
+
|
255
|
+
id = location.split("/").last
|
256
|
+
|
257
|
+
if location.include?("/TaskService/Tasks/")
|
258
|
+
wait_for_task(id)
|
259
|
+
else
|
260
|
+
wait_for_job(id) if respond_to?(:wait_for_job)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
|
266
|
+
def _perform_authenticated_request(method, path, options = {}, retry_count = 0)
|
267
|
+
if retry_count >= @retry_count
|
268
|
+
debug "Maximum retry count reached", 1, :red
|
269
|
+
raise Error, "Failed to authenticate after #{@retry_count} retries"
|
270
|
+
end
|
271
|
+
|
272
|
+
debug "Authenticated request: #{method.to_s.upcase} #{path}", 1
|
273
|
+
|
274
|
+
body = options[:body]
|
275
|
+
headers = options[:headers] || {}
|
276
|
+
timeout = options[:timeout]
|
277
|
+
open_timeout = options[:open_timeout]
|
278
|
+
|
279
|
+
headers['User-Agent'] ||= 'Supermicro Ruby Client'
|
280
|
+
headers['Accept'] ||= 'application/json'
|
281
|
+
headers['Host'] = @host_header if @host_header
|
282
|
+
|
283
|
+
if body && @verbosity >= 2
|
284
|
+
debug "Request body: #{body}", 2
|
285
|
+
end
|
286
|
+
|
287
|
+
if @direct_mode
|
288
|
+
headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
289
|
+
debug "Using Basic Auth for request (direct mode)", 2
|
290
|
+
elsif session.x_auth_token
|
291
|
+
headers['X-Auth-Token'] = session.x_auth_token
|
292
|
+
debug "Using X-Auth-Token for authentication", 2
|
293
|
+
end
|
294
|
+
|
295
|
+
response = make_request_with_timeouts(method, path, body, headers, timeout, open_timeout)
|
296
|
+
|
297
|
+
case response.status
|
298
|
+
when 301, 302, 303, 307, 308
|
299
|
+
# Handle redirects
|
300
|
+
if response.headers['location']
|
301
|
+
new_path = response.headers['location']
|
302
|
+
# If it's a relative path, keep it as is; if absolute URL, extract the path
|
303
|
+
if new_path.start_with?('http')
|
304
|
+
new_path = URI.parse(new_path).path
|
305
|
+
end
|
306
|
+
debug "Redirecting to: #{new_path}", 2
|
307
|
+
return _perform_authenticated_request(method, new_path, options, retry_count)
|
308
|
+
else
|
309
|
+
response
|
310
|
+
end
|
311
|
+
when 401, 403
|
312
|
+
handle_auth_failure(method, path, options, retry_count)
|
313
|
+
else
|
314
|
+
debug "Response status: #{response.status}", 2
|
315
|
+
response
|
316
|
+
end
|
317
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e
|
318
|
+
handle_connection_error(e, method, path, options, retry_count)
|
319
|
+
rescue => e
|
320
|
+
handle_general_error(e, method, path, options, retry_count)
|
321
|
+
end
|
322
|
+
|
323
|
+
def make_request_with_timeouts(method, path, body, headers, timeout, open_timeout)
|
324
|
+
conn = session.connection
|
325
|
+
original_timeout = conn.options.timeout
|
326
|
+
original_open_timeout = conn.options.open_timeout
|
327
|
+
|
328
|
+
begin
|
329
|
+
conn.options.timeout = timeout if timeout
|
330
|
+
conn.options.open_timeout = open_timeout if open_timeout
|
331
|
+
|
332
|
+
conn.run_request(method, path, body, headers)
|
333
|
+
ensure
|
334
|
+
conn.options.timeout = original_timeout
|
335
|
+
conn.options.open_timeout = original_open_timeout
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def handle_auth_failure(method, path, options, retry_count)
|
340
|
+
if @direct_mode
|
341
|
+
debug "Authentication failed in direct mode, retrying...", 1, :light_yellow
|
342
|
+
sleep(retry_count + 1)
|
343
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
344
|
+
else
|
345
|
+
debug "Session expired, creating new session...", 1, :light_yellow
|
346
|
+
session.delete if session.x_auth_token
|
347
|
+
|
348
|
+
if session.create
|
349
|
+
debug "New session created, retrying request...", 1, :green
|
350
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
351
|
+
else
|
352
|
+
debug "Session creation failed, falling back to direct mode...", 1, :light_yellow
|
353
|
+
@direct_mode = true
|
354
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def handle_connection_error(error, method, path, options, retry_count)
|
360
|
+
debug "Connection error: #{error.message}", 1, :red
|
361
|
+
sleep(retry_count + 1)
|
362
|
+
|
363
|
+
if @direct_mode || session.x_auth_token
|
364
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
365
|
+
elsif session.create
|
366
|
+
debug "Created new session after connection error", 1, :green
|
367
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
368
|
+
else
|
369
|
+
@direct_mode = true
|
370
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def handle_general_error(error, method, path, options, retry_count)
|
375
|
+
debug "Error during request: #{error.message}", 1, :red
|
376
|
+
|
377
|
+
if @direct_mode
|
378
|
+
raise Error, "Error during authenticated request: #{error.message}"
|
379
|
+
elsif session.create
|
380
|
+
debug "Created new session after error, retrying...", 1, :green
|
381
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
382
|
+
else
|
383
|
+
@direct_mode = true
|
384
|
+
return _perform_authenticated_request(method, path, options, retry_count + 1)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def _perform_get(path:, headers: {})
|
389
|
+
debug "GET request to #{base_url}/#{path}", 1
|
390
|
+
|
391
|
+
headers_to_use = {
|
392
|
+
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
|
393
|
+
"Accept-Encoding" => "deflate, gzip"
|
394
|
+
}
|
395
|
+
headers_to_use["Host"] = @host_header if @host_header
|
396
|
+
|
397
|
+
if @direct_mode
|
398
|
+
headers_to_use["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
399
|
+
debug "Using Basic Auth for GET request", 2
|
400
|
+
elsif session.x_auth_token
|
401
|
+
headers_to_use["X-Auth-Token"] = session.x_auth_token
|
402
|
+
debug "Using X-Auth-Token for GET request", 2
|
403
|
+
end
|
404
|
+
|
405
|
+
debug "Request headers: #{headers_to_use.merge(headers).inspect}", 3
|
406
|
+
|
407
|
+
response = HTTParty.get(
|
408
|
+
"#{base_url}/#{path}",
|
409
|
+
headers: headers_to_use.merge(headers),
|
410
|
+
verify: false
|
411
|
+
)
|
412
|
+
|
413
|
+
debug "Response status: #{response.code}", 1
|
414
|
+
debug "Response headers: #{response.headers.inspect}", 2
|
415
|
+
debug "Response body: #{response.body.to_s[0..500]}#{response.body.to_s.length > 500 ? '...' : ''}", 3 if response.body
|
416
|
+
|
417
|
+
response
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Supermicro
|
4
|
+
class Error < StandardError; end
|
5
|
+
class AuthenticationError < Error; end
|
6
|
+
class ConnectionError < Error; end
|
7
|
+
class NotFoundError < Error; end
|
8
|
+
class TimeoutError < Error; end
|
9
|
+
class BadRequestError < Error; end
|
10
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'colorize'
|
5
|
+
|
6
|
+
module Supermicro
|
7
|
+
module Jobs
|
8
|
+
def jobs
|
9
|
+
response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks?$expand=*($levels=1)")
|
10
|
+
|
11
|
+
if response.status == 200
|
12
|
+
begin
|
13
|
+
data = JSON.parse(response.body)
|
14
|
+
|
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
|
+
|
28
|
+
return tasks
|
29
|
+
rescue JSON::ParserError
|
30
|
+
raise Error, "Failed to parse tasks response: #{response.body}"
|
31
|
+
end
|
32
|
+
else
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def job_status(job_id)
|
38
|
+
response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}")
|
39
|
+
|
40
|
+
if response.status == 200
|
41
|
+
begin
|
42
|
+
data = JSON.parse(response.body)
|
43
|
+
|
44
|
+
{
|
45
|
+
"id" => data["Id"],
|
46
|
+
"name" => data["Name"],
|
47
|
+
"state" => data["TaskState"],
|
48
|
+
"status" => data["TaskStatus"],
|
49
|
+
"percent_complete" => data["PercentComplete"] || data.dig("Oem", "Supermicro", "PercentComplete"),
|
50
|
+
"start_time" => data["StartTime"],
|
51
|
+
"end_time" => data["EndTime"],
|
52
|
+
"messages" => data["Messages"]
|
53
|
+
}
|
54
|
+
rescue JSON::ParserError
|
55
|
+
raise Error, "Failed to parse job status response: #{response.body}"
|
56
|
+
end
|
57
|
+
else
|
58
|
+
raise Error, "Failed to get job status. Status code: #{response.status}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def wait_for_job(job_id, timeout: 600)
|
63
|
+
start_time = Time.now
|
64
|
+
|
65
|
+
puts "Waiting for job #{job_id} to complete...".yellow
|
66
|
+
|
67
|
+
loop do
|
68
|
+
if Time.now - start_time > timeout
|
69
|
+
raise Error, "Job #{job_id} timed out after #{timeout} seconds"
|
70
|
+
end
|
71
|
+
|
72
|
+
begin
|
73
|
+
status = job_status(job_id)
|
74
|
+
|
75
|
+
case status["state"]
|
76
|
+
when "Completed", "Killed", "Exception", "Cancelled"
|
77
|
+
if status["status"] == "OK" || status["status"] == "Completed"
|
78
|
+
puts "Job completed successfully.".green
|
79
|
+
return { status: :success, job: status }
|
80
|
+
else
|
81
|
+
puts "Job failed: #{status["status"]}".red
|
82
|
+
return { status: :failed, job: status, error: status["messages"] }
|
83
|
+
end
|
84
|
+
when "Running", "Starting", "New", "Pending"
|
85
|
+
percent = status["percent_complete"]
|
86
|
+
if percent
|
87
|
+
puts "Job progress: #{percent}%".cyan
|
88
|
+
else
|
89
|
+
puts "Job is #{status["state"]}...".cyan
|
90
|
+
end
|
91
|
+
sleep 5
|
92
|
+
else
|
93
|
+
puts "Unknown job state: #{status["state"]}".yellow
|
94
|
+
sleep 5
|
95
|
+
end
|
96
|
+
rescue => e
|
97
|
+
debug "Error checking job status: #{e.message}", 1, :yellow
|
98
|
+
sleep 5
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def cancel_job(job_id)
|
104
|
+
puts "Cancelling job #{job_id}...".yellow
|
105
|
+
|
106
|
+
response = authenticated_request(
|
107
|
+
:delete,
|
108
|
+
"/redfish/v1/TaskService/Tasks/#{job_id}"
|
109
|
+
)
|
110
|
+
|
111
|
+
if response.status.between?(200, 299)
|
112
|
+
puts "Job cancelled successfully.".green
|
113
|
+
return true
|
114
|
+
else
|
115
|
+
raise Error, "Failed to cancel job: #{response.status} - #{response.body}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def clear_completed_jobs
|
120
|
+
all_jobs = jobs
|
121
|
+
completed = all_jobs.select { |j| j["state"] == "Completed" }
|
122
|
+
|
123
|
+
if completed.empty?
|
124
|
+
puts "No completed jobs to clear.".yellow
|
125
|
+
return true
|
126
|
+
end
|
127
|
+
|
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
|
143
|
+
end
|
144
|
+
rescue => e
|
145
|
+
puts " Error clearing #{job["id"]}: #{e.message}".red
|
146
|
+
success = false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
success
|
151
|
+
end
|
152
|
+
|
153
|
+
def jobs_summary
|
154
|
+
all_jobs = jobs
|
155
|
+
|
156
|
+
puts "\n=== Jobs Summary ===".green
|
157
|
+
|
158
|
+
if all_jobs.empty?
|
159
|
+
puts "No jobs found.".yellow
|
160
|
+
return all_jobs
|
161
|
+
end
|
162
|
+
|
163
|
+
by_state = all_jobs.group_by { |j| j["state"] }
|
164
|
+
|
165
|
+
by_state.each do |state, state_jobs|
|
166
|
+
puts "\n#{state}:".cyan
|
167
|
+
state_jobs.each do |job|
|
168
|
+
percent = job["percent_complete"] ? " (#{job["percent_complete"]}%)" : ""
|
169
|
+
puts " #{job["name"]} - #{job["status"]}#{percent}".light_cyan
|
170
|
+
puts " ID: #{job["id"]}"
|
171
|
+
puts " Started: #{job["start_time"]}" if job["start_time"]
|
172
|
+
puts " Ended: #{job["end_time"]}" if job["end_time"]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
all_jobs
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|