idrac 0.1.92 → 0.3.2

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.
data/lib/idrac/boot.rb ADDED
@@ -0,0 +1,357 @@
1
+ require 'json'
2
+ require 'colorize'
3
+
4
+ module IDRAC
5
+ module BootManagementMethods
6
+ # Get BIOS boot options
7
+ def get_bios_boot_options
8
+ response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootSources")
9
+
10
+ if response.status == 200
11
+ begin
12
+ data = JSON.parse(response.body)
13
+
14
+ if data["Attributes"]["UefiBootSeq"].blank?
15
+ puts "Not in UEFI mode".red
16
+ return false
17
+ end
18
+
19
+ boot_order = []
20
+ boot_options = []
21
+
22
+ data["Attributes"]["UefiBootSeq"].each do |seq|
23
+ puts "#{seq["Name"]} > #{seq["Enabled"]}".yellow
24
+ boot_options << seq["Name"]
25
+ boot_order << seq["Name"] if seq["Enabled"]
26
+ end
27
+
28
+ return {
29
+ boot_options: boot_options,
30
+ boot_order: boot_order
31
+ }
32
+ rescue JSON::ParserError
33
+ raise Error, "Failed to parse BIOS boot options response: #{response.body}"
34
+ end
35
+ else
36
+ raise Error, "Failed to get BIOS boot options. Status code: #{response.status}"
37
+ end
38
+ end
39
+
40
+ # Ensure UEFI boot mode
41
+ def ensure_uefi_boot
42
+ response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Bios")
43
+
44
+ if response.status == 200
45
+ begin
46
+ data = JSON.parse(response.body)
47
+
48
+ if data["Attributes"]["BootMode"] == "Uefi"
49
+ puts "System is already in UEFI boot mode".green
50
+ return true
51
+ else
52
+ puts "System is not in UEFI boot mode. Setting to UEFI...".yellow
53
+
54
+ # Create payload for UEFI boot mode
55
+ payload = {
56
+ "Attributes": {
57
+ "BootMode": "Uefi"
58
+ }
59
+ }
60
+
61
+ # If iDRAC 9, we need to enable HddPlaceholder
62
+ if get_idrac_version == 9
63
+ payload[:Attributes][:HddPlaceholder] = "Enabled"
64
+ end
65
+
66
+ response = authenticated_request(
67
+ :patch,
68
+ "/redfish/v1/Systems/System.Embedded.1/Bios/Settings",
69
+ body: payload.to_json,
70
+ headers: { 'Content-Type': 'application/json' }
71
+ )
72
+
73
+ if response.status.between?(200, 299)
74
+ puts "UEFI boot mode set. A system reboot is required for changes to take effect.".green
75
+
76
+ # Check for job creation
77
+ if response.headers["Location"]
78
+ job_id = response.headers["Location"].split("/").last
79
+ wait_for_job(job_id)
80
+ end
81
+
82
+ return true
83
+ else
84
+ error_message = "Failed to set UEFI boot mode. Status code: #{response.status}"
85
+
86
+ begin
87
+ error_data = JSON.parse(response.body)
88
+ if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
89
+ error_info = error_data["error"]["@Message.ExtendedInfo"].first
90
+ error_message += ", Message: #{error_info['Message']}"
91
+ end
92
+ rescue
93
+ # Ignore JSON parsing errors
94
+ end
95
+
96
+ raise Error, error_message
97
+ end
98
+ end
99
+ rescue JSON::ParserError
100
+ raise Error, "Failed to parse BIOS response: #{response.body}"
101
+ end
102
+ else
103
+ raise Error, "Failed to get BIOS information. Status code: #{response.status}"
104
+ end
105
+ end
106
+
107
+ # Set boot order (HD first)
108
+ def set_boot_order_hd_first
109
+ # First ensure we're in UEFI mode
110
+ ensure_uefi_boot
111
+
112
+ # Get available boot options
113
+ boot_options_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootOptions?$expand=*($levels=1)")
114
+
115
+ if boot_options_response.status == 200
116
+ begin
117
+ data = JSON.parse(boot_options_response.body)
118
+
119
+ puts "Available boot options:"
120
+ data["Members"].each { |m| puts "\t#{m['DisplayName']} -> #{m['Id']}" }
121
+
122
+ # Find RAID controller or HD
123
+ device = data["Members"].find { |m| m["DisplayName"] =~ /RAID Controller/ }
124
+ # Sometimes it's named differently
125
+ device ||= data["Members"].find { |m| m["DisplayName"] =~ /ubuntu/i }
126
+ device ||= data["Members"].find { |m| m["DisplayName"] =~ /UEFI Hard Drive/i }
127
+ device ||= data["Members"].find { |m| m["DisplayName"] =~ /Hard Drive/i }
128
+
129
+ if device.nil?
130
+ raise Error, "No bootable hard drive or RAID controller found in boot options"
131
+ end
132
+
133
+ boot_id = device["Id"]
134
+
135
+ # Set boot order
136
+ response = authenticated_request(
137
+ :patch,
138
+ "/redfish/v1/Systems/System.Embedded.1",
139
+ body: { "Boot": { "BootOrder": [boot_id] } }.to_json,
140
+ headers: { 'Content-Type': 'application/json' }
141
+ )
142
+
143
+ if response.status.between?(200, 299)
144
+ puts "Boot order set to HD first".green
145
+ return true
146
+ else
147
+ error_message = "Failed to set boot order. Status code: #{response.status}"
148
+
149
+ begin
150
+ error_data = JSON.parse(response.body)
151
+ if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
152
+ error_info = error_data["error"]["@Message.ExtendedInfo"].first
153
+ error_message += ", Message: #{error_info['Message']}"
154
+ end
155
+ rescue
156
+ # Ignore JSON parsing errors
157
+ end
158
+
159
+ raise Error, error_message
160
+ end
161
+ rescue JSON::ParserError
162
+ raise Error, "Failed to parse boot options response: #{response.body}"
163
+ end
164
+ else
165
+ raise Error, "Failed to get boot options. Status code: #{boot_options_response.status}"
166
+ end
167
+ end
168
+
169
+ # Configure BIOS settings
170
+ def configure_bios_settings(settings)
171
+ response = authenticated_request(
172
+ :patch,
173
+ "/redfish/v1/Systems/System.Embedded.1/Bios/Settings",
174
+ body: { "Attributes": settings }.to_json,
175
+ headers: { 'Content-Type': 'application/json' }
176
+ )
177
+
178
+ if response.status.between?(200, 299)
179
+ puts "BIOS settings configured. A system reboot is required for changes to take effect.".green
180
+
181
+ # Check if we need to wait for a job
182
+ if response.headers["Location"]
183
+ job_id = response.headers["Location"].split("/").last
184
+ wait_for_job(job_id)
185
+ end
186
+
187
+ return true
188
+ else
189
+ error_message = "Failed to configure BIOS settings. Status code: #{response.status}"
190
+
191
+ begin
192
+ error_data = JSON.parse(response.body)
193
+ if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
194
+ error_info = error_data["error"]["@Message.ExtendedInfo"].first
195
+ error_message += ", Message: #{error_info['Message']}"
196
+ end
197
+ rescue
198
+ # Ignore JSON parsing errors
199
+ end
200
+
201
+ raise Error, error_message
202
+ end
203
+ end
204
+
205
+ # Configure BIOS to optimize for OS power management
206
+ def set_bios_os_power_control
207
+ settings = {
208
+ "ProcCStates": "Enabled", # Processor C-States
209
+ "SysProfile": "PerfPerWattOptimizedOs",
210
+ "ProcPwrPerf": "OsDbpm", # OS Power Management
211
+ "PcieAspmL1": "Enabled" # PCIe Active State Power Management
212
+ }
213
+
214
+ configure_bios_settings(settings)
215
+ end
216
+
217
+ # Configure BIOS to ignore boot errors
218
+ def set_bios_ignore_errors(value = true)
219
+ configure_bios_settings({
220
+ "ErrPrompt": value ? "Disabled" : "Enabled"
221
+ })
222
+ end
223
+
224
+ # Get iDRAC version - needed for boot management differences
225
+ def get_idrac_version
226
+ response = authenticated_request(:get, "/redfish/v1")
227
+
228
+ if response.status == 200
229
+ begin
230
+ data = JSON.parse(response.body)
231
+ redfish = data["RedfishVersion"]
232
+ server = response.headers["server"]
233
+
234
+ case server.to_s.downcase
235
+ when /appweb\/4.5.4/, /idrac\/8/
236
+ return 8
237
+ when /apache/, /idrac\/9/
238
+ return 9
239
+ else
240
+ # Try to determine by RedfishVersion as fallback
241
+ if redfish == "1.4.0"
242
+ return 8
243
+ elsif redfish == "1.18.0"
244
+ return 9
245
+ else
246
+ raise Error, "Unknown iDRAC version: #{server} / #{redfish}"
247
+ end
248
+ end
249
+ rescue JSON::ParserError
250
+ raise Error, "Failed to parse iDRAC response: #{response.body}"
251
+ end
252
+ else
253
+ raise Error, "Failed to get iDRAC information. Status code: #{response.status}"
254
+ end
255
+ end
256
+
257
+ # Create System Configuration Profile for BIOS settings
258
+ def create_scp_for_bios(settings)
259
+ attributes = []
260
+
261
+ settings.each do |key, value|
262
+ attributes << {
263
+ "Name": key.to_s,
264
+ "Value": value,
265
+ "Set On Import": "True"
266
+ }
267
+ end
268
+
269
+ scp = {
270
+ "SystemConfiguration": {
271
+ "Components": [
272
+ {
273
+ "FQDD": "BIOS.Setup.1-1",
274
+ "Attributes": attributes
275
+ }
276
+ ]
277
+ }
278
+ }
279
+
280
+ return scp
281
+ end
282
+
283
+ # Import System Configuration Profile for advanced configurations
284
+ def import_system_configuration(scp, target: "ALL", reboot: false)
285
+ params = {
286
+ "ImportBuffer": JSON.pretty_generate(scp),
287
+ "ShareParameters": {
288
+ "Target": target
289
+ }
290
+ }
291
+
292
+ # Configure shutdown behavior
293
+ params["ShutdownType"] = "Forced"
294
+ params["HostPowerState"] = reboot ? "On" : "Off"
295
+
296
+ response = authenticated_request(
297
+ :post,
298
+ "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
299
+ body: params.to_json,
300
+ headers: { 'Content-Type': 'application/json' }
301
+ )
302
+
303
+ if response.status.between?(200, 299)
304
+ # Check if we need to wait for a job
305
+ if response.headers["location"]
306
+ job_id = response.headers["location"].split("/").last
307
+
308
+ job = wait_for_job(job_id)
309
+
310
+ # Check for task completion status
311
+ if job["TaskState"] == "Completed" && job["TaskStatus"] == "OK"
312
+ puts "System configuration imported successfully".green
313
+ return true
314
+ else
315
+ # If there's an error message with a line number, surface it
316
+ error_message = "Failed to import system configuration"
317
+
318
+ if job["Messages"]
319
+ job["Messages"].each do |m|
320
+ puts "#{m["Message"]} (#{m["Severity"]})".red
321
+
322
+ # Check for line number in error message
323
+ if m["Message"] =~ /line (\d+)/
324
+ line_num = $1.to_i
325
+ lines = JSON.pretty_generate(scp).split("\n")
326
+ puts "Error near line #{line_num}:".red
327
+ ((line_num-3)..(line_num+1)).each do |ln|
328
+ puts "#{ln}: #{lines[ln-1]}" if ln > 0 && ln <= lines.length
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ raise Error, error_message
335
+ end
336
+ else
337
+ puts "System configuration import started, but no job ID was returned".yellow
338
+ return true
339
+ end
340
+ else
341
+ error_message = "Failed to import system configuration. Status code: #{response.status}"
342
+
343
+ begin
344
+ error_data = JSON.parse(response.body)
345
+ if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
346
+ error_info = error_data["error"]["@Message.ExtendedInfo"].first
347
+ error_message += ", Message: #{error_info['Message']}"
348
+ end
349
+ rescue
350
+ # Ignore JSON parsing errors
351
+ end
352
+
353
+ raise Error, error_message
354
+ end
355
+ end
356
+ end
357
+ end
data/lib/idrac/client.rb CHANGED
@@ -10,15 +10,19 @@ require 'colorize'
10
10
  module IDRAC
11
11
  class Client
12
12
  attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :auto_delete_sessions, :session, :web
13
- attr_accessor :direct_mode, :verbosity
13
+ attr_accessor :direct_mode, :verbosity, :retry_count, :retry_delay
14
14
 
15
15
  include PowerMethods
16
16
  include SessionMethods
17
17
  include Debuggable
18
18
  include JobMethods
19
19
  include LifecycleMethods
20
+ include StorageMethods
21
+ include SystemComponentMethods
22
+ include VirtualMediaMethods
23
+ include BootManagementMethods
20
24
 
21
- def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, auto_delete_sessions: true)
25
+ def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, auto_delete_sessions: true, retry_count: 3, retry_delay: 1)
22
26
  @host = host
23
27
  @username = username
24
28
  @password = password
@@ -28,6 +32,8 @@ module IDRAC
28
32
  @direct_mode = direct_mode
29
33
  @auto_delete_sessions = auto_delete_sessions
30
34
  @verbosity = 0
35
+ @retry_count = retry_count
36
+ @retry_delay = retry_delay
31
37
 
32
38
  # Initialize the session and web classes
33
39
  @session = Session.new(self)
@@ -76,107 +82,165 @@ module IDRAC
76
82
  return true
77
83
  end
78
84
 
79
- # Make an authenticated request to the iDRAC
80
- def authenticated_request(method, path, options = {}, retry_count = 0)
81
- # Limit retries to prevent infinite loops
82
- if retry_count >= 3
83
- debug "Maximum retry count reached for authenticated request", 1, :red
84
- raise Error, "Maximum retry count reached for authenticated request"
85
+ # Send an authenticated request to the iDRAC
86
+ def authenticated_request(method, path, options = {})
87
+ with_retries do
88
+ _perform_authenticated_request(method, path, options)
85
89
  end
90
+ end
91
+
92
+ def get(path:, headers: {})
93
+ with_retries do
94
+ _perform_get(path: path, headers: headers)
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # Implementation of authenticated request without retry logic
101
+ def _perform_authenticated_request(method, path, options = {}, retry_count = 0)
102
+ # Check retry count to prevent infinite recursion
103
+ if retry_count >= @retry_count
104
+ debug "Maximum retry count reached", 1, :red
105
+ raise Error, "Failed to authenticate after #{@retry_count} retries"
106
+ end
107
+
108
+ # Form the full URL
109
+ full_url = "#{base_url}/redfish/v1".chomp('/') + '/' + path.sub(/^\//, '')
86
110
 
111
+ # Log the request
87
112
  debug "Authenticated request: #{method.to_s.upcase} #{path}", 1
88
113
 
114
+ # Extract options
115
+ body = options[:body]
116
+ headers = options[:headers] || {}
117
+
118
+ # Add client headers
119
+ headers['User-Agent'] ||= 'iDRAC Ruby Client'
120
+ headers['Accept'] ||= 'application/json'
121
+
89
122
  # If we're in direct mode, use Basic Auth
90
123
  if @direct_mode
91
124
  # Create Basic Auth header
92
125
  auth_header = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
126
+ headers['Authorization'] = auth_header
127
+ debug "Using Basic Auth for request (direct mode)", 2
93
128
 
94
- # Add the Authorization header to the request
95
- options[:headers] ||= {}
96
- options[:headers]['Authorization'] = auth_header
97
-
98
- debug "Using Basic Auth for request", 2
99
-
100
- # Make the request
101
129
  begin
102
- response = connection.send(method, path) do |req|
103
- req.headers.merge!(options[:headers])
104
- req.body = options[:body] if options[:body]
105
- end
130
+ # Make the request directly
131
+ response = session.connection.run_request(
132
+ method,
133
+ path.sub(/^\//, ''),
134
+ body,
135
+ headers
136
+ )
137
+
138
+ debug "Response status: #{response.status}", 2
106
139
 
107
- debug "Response status: #{response.status}", 1
108
- debug "Response headers: #{response.headers.inspect}", 2
109
- debug "Response body: #{response.body}", 3 if response.body
140
+ # Even in direct mode, check for authentication issues
141
+ if response.status == 401 || response.status == 403
142
+ debug "Authentication failed in direct mode, retrying with new credentials...", 1, :light_yellow
143
+ sleep(retry_count + 1) # Add some delay before retry
144
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
145
+ end
110
146
 
111
147
  return response
148
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
149
+ debug "Connection error in direct mode: #{e.message}", 1, :red
150
+ sleep(retry_count + 1) # Add some delay before retry
151
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
112
152
  rescue => e
113
- debug "Error during authenticated request (direct mode): #{e.message}", 1, :red
153
+ debug "Error during direct mode request: #{e.message}", 1, :red
114
154
  raise Error, "Error during authenticated request: #{e.message}"
115
155
  end
116
- else
117
- # Use X-Auth-Token if available
118
- if session.x_auth_token
119
- # Add the X-Auth-Token header to the request
120
- options[:headers] ||= {}
121
- options[:headers]['X-Auth-Token'] = session.x_auth_token
156
+ # Use Redfish session token if available
157
+ elsif session.x_auth_token
158
+ begin
159
+ headers['X-Auth-Token'] = session.x_auth_token
122
160
 
123
- debug "Using X-Auth-Token for request", 2
161
+ debug "Using X-Auth-Token for authentication", 2
162
+ debug "Request headers: #{headers.reject { |k,v| k =~ /auth/i }.to_json}", 3
163
+ debug "Request body: #{body.to_s[0..500]}", 3 if body
124
164
 
125
- # Make the request
126
- begin
127
- response = connection.send(method, path) do |req|
128
- req.headers.merge!(options[:headers])
129
- req.body = options[:body] if options[:body]
130
- end
131
-
132
- debug "Response status: #{response.status}", 1
133
- debug "Response headers: #{response.headers.inspect}", 2
134
- debug "Response body: #{response.body}", 3 if response.body
165
+ response = session.connection.run_request(
166
+ method,
167
+ path.sub(/^\//, ''),
168
+ body,
169
+ headers
170
+ )
171
+
172
+ debug "Response status: #{response.status}", 2
173
+ debug "Response headers: #{response.headers.to_json}", 3
174
+ debug "Response body: #{response.body.to_s[0..500]}", 3 if response.body
175
+
176
+ # Handle session expiration
177
+ if response.status == 401 || response.status == 403
178
+ debug "Session expired or invalid, creating a new session...", 1, :light_yellow
135
179
 
136
- # Check if the session is still valid
137
- if response.status == 401 || response.status == 403
138
- debug "Session expired or invalid, attempting to create a new session...", 1, :light_yellow
139
-
140
- # Try to create a new session
141
- if session.create
142
- debug "Successfully created a new session, retrying request...", 1, :green
143
- return authenticated_request(method, path, options, retry_count + 1)
144
- else
145
- debug "Failed to create a new session, falling back to direct mode...", 1, :light_yellow
146
- @direct_mode = true
147
- return authenticated_request(method, path, options, retry_count + 1)
148
- end
180
+ # If session.delete returns true, the session was successfully deleted
181
+ if session.delete
182
+ debug "Successfully cleared expired session", 1, :green
149
183
  end
150
184
 
151
- return response
152
- rescue => e
153
- debug "Error during authenticated request (token mode): #{e.message}", 1, :red
154
-
155
185
  # Try to create a new session
156
186
  if session.create
157
- debug "Successfully created a new session after error, retrying request...", 1, :green
158
- return authenticated_request(method, path, options, retry_count + 1)
187
+ debug "Successfully created a new session after expiration, retrying request...", 1, :green
188
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
159
189
  else
160
- debug "Failed to create a new session after error, falling back to direct mode...", 1, :light_yellow
190
+ debug "Failed to create a new session after expiration, falling back to direct mode...", 1, :light_yellow
161
191
  @direct_mode = true
162
- return authenticated_request(method, path, options, retry_count + 1)
192
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
163
193
  end
164
194
  end
165
- else
166
- # If we don't have a token, try to create a session
195
+
196
+ return response
197
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
198
+ debug "Connection error: #{e.message}", 1, :red
199
+ sleep(retry_count + 1) # Add some delay before retry
200
+
201
+ # If we still have the token, try to reuse it
202
+ if session.x_auth_token
203
+ debug "Retrying with existing token after connection error", 1, :light_yellow
204
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
205
+ else
206
+ # Otherwise try to create a new session
207
+ debug "Trying to create a new session after connection error", 1, :light_yellow
208
+ if session.create
209
+ debug "Successfully created a new session after connection error", 1, :green
210
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
211
+ else
212
+ debug "Failed to create session after connection error, falling back to direct mode", 1, :light_yellow
213
+ @direct_mode = true
214
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
215
+ end
216
+ end
217
+ rescue => e
218
+ debug "Error during authenticated request (token mode): #{e.message}", 1, :red
219
+
220
+ # Try to create a new session
167
221
  if session.create
168
- debug "Successfully created a new session, making request...", 1, :green
169
- return authenticated_request(method, path, options, retry_count + 1)
222
+ debug "Successfully created a new session after error, retrying request...", 1, :green
223
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
170
224
  else
171
- debug "Failed to create a session, falling back to direct mode...", 1, :light_yellow
225
+ debug "Failed to create a new session after error, falling back to direct mode...", 1, :light_yellow
172
226
  @direct_mode = true
173
- return authenticated_request(method, path, options, retry_count + 1)
227
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
174
228
  end
175
229
  end
230
+ else
231
+ # If we don't have a token, try to create a session
232
+ if session.create
233
+ debug "Successfully created a new session, making request...", 1, :green
234
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
235
+ else
236
+ debug "Failed to create a session, falling back to direct mode...", 1, :light_yellow
237
+ @direct_mode = true
238
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
239
+ end
176
240
  end
177
241
  end
178
242
 
179
- def get(path:, headers: {})
243
+ def _perform_get(path:, headers: {})
180
244
  # For screenshot functionality, we need to use the WebUI cookies
181
245
  if web.cookies.nil? && path.include?('screen/screen.jpg')
182
246
  web.login unless web.session_id
@@ -216,6 +280,8 @@ module IDRAC
216
280
  response
217
281
  end
218
282
 
283
+ public
284
+
219
285
  def screenshot
220
286
  web.capture_screenshot
221
287
  end
@@ -234,5 +300,34 @@ module IDRAC
234
300
  raise Error, "Failed to get Redfish version: #{response.status} - #{response.body}"
235
301
  end
236
302
  end
303
+
304
+ # Execute a block with automatic retries
305
+ # @param max_retries [Integer] Maximum number of retry attempts
306
+ # @param initial_delay [Integer] Initial delay in seconds between retries (increases exponentially)
307
+ # @param error_classes [Array] Array of error classes to catch and retry
308
+ # @yield The block to execute with retries
309
+ # @return [Object] The result of the block
310
+ def with_retries(max_retries = nil, initial_delay = nil, error_classes = nil)
311
+ # Use instance variables if not specified
312
+ max_retries ||= @retry_count
313
+ initial_delay ||= @retry_delay
314
+ error_classes ||= [StandardError]
315
+
316
+ retries = 0
317
+ begin
318
+ yield
319
+ rescue *error_classes => e
320
+ retries += 1
321
+ if retries <= max_retries
322
+ delay = initial_delay * (retries ** 1.5).to_i # Exponential backoff
323
+ debug "RETRY: #{e.message} - Attempt #{retries}/#{max_retries}, waiting #{delay}s", 1, :yellow
324
+ sleep delay
325
+ retry
326
+ else
327
+ debug "MAX RETRIES REACHED: #{e.message} after #{max_retries} attempts", 1, :red
328
+ raise e
329
+ end
330
+ end
331
+ end
237
332
  end
238
333
  end