cem_acpt 0.3.2-universal-java-17 → 0.3.4-universal-java-17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../logging'
4
+ require_relative 'workflow'
5
+
6
+ module CemAcpt
7
+ module TestRunner
8
+ # RunnerWorkflowBuilder builds a workflow for a TestRunner
9
+ # @!attribute [r] workflow
10
+ # @return [CemAcpt::TestRunner::Workflow::Manager] Workflow object
11
+ class RunnerWorkflowBuilder
12
+ include CemAcpt::LoggingAsync
13
+
14
+ attr_reader :workflow
15
+
16
+ # @param node [CemAcpt::Platform::Base] Initialized node object
17
+ # @param config [Hash] Context config hash
18
+ def initialize(node, config)
19
+ @node = node
20
+ @workflow = CemAcpt::TestRunner::Workflow::Manager.new(workflow_manager_opts(config))
21
+ @config = config
22
+ end
23
+
24
+ def add_sleep(**kwargs)
25
+ opts = {
26
+ node: @node,
27
+ time: kwargs[:time] || 10,
28
+ retryable: false,
29
+ }
30
+ @workflow.add_step(:sleep, **opts) do |s|
31
+ log_info("sleeping for #{s.opts[:time]} seconds", s)
32
+ sleep(s.opts[:time])
33
+ end
34
+ end
35
+
36
+ def add_provision(**kwargs)
37
+ opts = {
38
+ node: @node,
39
+ retryable: kwargs[:retryable] || false,
40
+ }
41
+ @workflow.add_step(:provision, **opts) do |s|
42
+ s.opts[:node].provision
43
+ s.opts[:node]
44
+ end
45
+ end
46
+
47
+ def add_wait_for_node_ssh(**kwargs)
48
+ opts = {
49
+ node: @node,
50
+ retryable: kwargs[:retryable] || true,
51
+ retry_delay: kwargs[:retry_delay] || 30,
52
+ retry_max: kwargs[:retry_max] || 10,
53
+ }
54
+ @workflow.add_step(:wait_for_node_ssh, **opts) do |s|
55
+ unless s.opts[:node].ready?
56
+ raise "wait_for_node_ssh timed out for node #{s.opts[:node].node_name}"
57
+ end
58
+ s.opts[:node]
59
+ end
60
+ end
61
+
62
+ def add_check_dnf_automatic(**kwargs)
63
+ opts = {
64
+ node: @node,
65
+ retryable: kwargs[:retryable] || true,
66
+ retry_delay: kwargs[:retry_delay] || 30,
67
+ retry_workflow_on_fail: kwargs[:retry_workflow_on_fail] || true,
68
+ }
69
+ @workflow.add_step(:check_dnf_automatic, **opts) do |s|
70
+ unless s.opts[:node].dnf_automatic_success?
71
+ raise "dnf_automatic failed on node #{s.opts[:node].node_name}"
72
+ end
73
+ s.opts[:node]
74
+ end
75
+ end
76
+
77
+ def add_check_rpm_db(**kwargs)
78
+ opts = {
79
+ node: @node,
80
+ retryable: kwargs[:retryable] || true,
81
+ retry_delay: kwargs[:retry_delay] || 30,
82
+ retry_workflow_on_fail: kwargs[:retry_workflow_on_fail] || true,
83
+ }
84
+ @workflow.add_step(:rpm_db_check, **opts) do |s|
85
+ unless s.opts[:node].rpm_db_check_success?
86
+ raise "rpm_db_check failed on node #{s.opts[:node].node_name}"
87
+ end
88
+ s.opts[:node]
89
+ end
90
+ end
91
+
92
+ def add_save_node_to_inventory(node_inventory:, platform:, **kwargs)
93
+ opts = {
94
+ node: @node,
95
+ platform: platform,
96
+ node_inventory: node_inventory,
97
+ retryable: kwargs[:retryable] || false,
98
+ }
99
+ @workflow.add_step(:save_node_to_inventory, **opts) do |s|
100
+ node_desc = {
101
+ test_data: s.opts[:node].test_data,
102
+ platform: s.opts[:platform],
103
+ local_port: s.opts[:node].local_port,
104
+ }.merge(s.opts[:node].node)
105
+ s.opts[:node_inventory].add(s.opts[:node].node_name, node_desc)
106
+ s.opts[:node_inventory].save
107
+ log_info("node #{s.opts[:node_name]} saved to inventory", s)
108
+ s.opts[:node_inventory]
109
+ end
110
+ end
111
+
112
+ def add_check_for_module_package_path(module_pkg_path:, **kwargs)
113
+ opts = {
114
+ module_pkg_path: module_pkg_path,
115
+ retryable: kwargs[:retryable] || true,
116
+ retry_delay: kwargs[:retry_delay] || 30,
117
+ }
118
+ @workflow.add_step(:check_for_module_package_path, **opts) do |s|
119
+ unless File.exist?(s.opts[:module_pkg_path])
120
+ raise "module package #{s.opts[:module_pkg_path]} does not exist"
121
+ end
122
+ s.opts[:module_pkg_path]
123
+ end
124
+ end
125
+
126
+ def add_install_module(module_pkg_path:, **kwargs)
127
+ opts = {
128
+ node: @node,
129
+ module_pkg_path: module_pkg_path,
130
+ retryable: kwargs[:retryable] || true,
131
+ retry_delay: kwargs[:retry_delay] || 60,
132
+ }
133
+ @workflow.add_step(:bootstrap, **opts) do |s|
134
+ s.opts[:node].install_puppet_module_package(s.opts[:module_pkg_path])
135
+ s.opts[:node]
136
+ end
137
+ end
138
+
139
+ def add_check_node_inventory_file(node_inventory:, **kwargs)
140
+ opts = {
141
+ ni_file_path: node_inventory.save_file_path,
142
+ retryable: kwargs[:retryable] || true,
143
+ retry_max: kwargs[:retry_max] || 60,
144
+ retry_delay: kwargs[:retry_delay] || 5,
145
+ }
146
+ @workflow.add_step(:check_node_inventory_file, **opts) do |s|
147
+ unless File.exist?(s.opts[:ni_file_path])
148
+ raise "node inventory file #{s.opts[:ni_file_path]} not found"
149
+ end
150
+ s.opts[:ni_file_path]
151
+ end
152
+ end
153
+
154
+ def add_run_tests(rspec_opts:, rspec_cmd:, run_result:, **kwargs)
155
+ opts = {
156
+ node: @node,
157
+ rspec_opts: rspec_opts,
158
+ rspec_cmd: rspec_cmd,
159
+ run_result: run_result,
160
+ retryable: kwargs[:retryable] || false,
161
+ }
162
+ @workflow.add_step(:run_tests, **opts) do |s|
163
+ s.opts[:node].run_tests do |cmd_env|
164
+ cmd_opts = s.opts[:rspec_opts].dup
165
+ cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
166
+ rspec_cmd = s.opts[:rspec_cmd].new(cmd_opts)
167
+ run_result = s.opts[:run_result].dup
168
+ begin
169
+ rspec_cmd.execute(pty: false, log_prefix: "RSPEC: #{@node.test_data[:test_name]}")
170
+ run_result.from_json_file(cmd_opts.format[:json])
171
+ rescue Errno::EIO => e
172
+ log_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", s)
173
+ run_result.from_error(e)
174
+ rescue StandardError => e
175
+ log_error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}", s)
176
+ log_debug(e.backtrace.join("\n"), s)
177
+ run_result.from_error(e)
178
+ end
179
+ end
180
+ run_result
181
+ end
182
+ end
183
+
184
+ def add_clean_up(force: false, **kwargs)
185
+ opts = {
186
+ node: @node,
187
+ config: @config[:config],
188
+ force: force,
189
+ retryable: kwargs[:retryable] || false,
190
+ }
191
+ @workflow.add_step(:clean_up, **opts) do |s|
192
+ if !force && s.opts[:config].get('no_destroy_nodes')
193
+ log_info("not destroying node #{s.opts[:node].node_name} because 'no_destroy_nodes' is set to true", s)
194
+ else
195
+ log_info("destroying node #{s.opts[:node].node_name}", s)
196
+ s.opts[:node].destroy
197
+ log_info("node #{s.opts[:node].node_name} destroyed", s)
198
+ end
199
+ end
200
+ end
201
+
202
+ private
203
+
204
+ def log_msg(msg, step)
205
+ "Step '#{step.name}' on #{@node.node_name}: #{msg}"
206
+ end
207
+
208
+ def log_debug(msg, step)
209
+ async_debug(log_msg(msg, step))
210
+ end
211
+
212
+ def log_info(msg, step)
213
+ async_info(log_msg(msg, step))
214
+ end
215
+
216
+ def log_warn(msg, step)
217
+ async_warn(log_msg(msg, step))
218
+ end
219
+
220
+ def log_error(msg, step)
221
+ async_error(log_msg(msg, step))
222
+ end
223
+
224
+ def log_fatal(msg, step)
225
+ async_fatal(log_msg(msg, step))
226
+ end
227
+
228
+ def workflow_manager_opts(config)
229
+ {
230
+ retry_max: config[:workflow_retry_max] || 3,
231
+ retry_delay: config[:workflow_retry_delay] || 0,
232
+ ignore_failures: config[:workflow_ignore_failures] || false,
233
+ raise_on_fail: config[:workflow_raise_on_fail] || true,
234
+ }
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../logging'
4
+ require_relative 'step'
5
+
6
+ module CemAcpt
7
+ module TestRunner
8
+ module Workflow
9
+ # Manager is a class that manages how steps in a workflow are executed.
10
+ # @!attribute [r] completed_steps
11
+ # @return [Array<Step>] The steps that have been completed
12
+ # @!attribute [r] last_error
13
+ # @return [Exception, nil] The last error that occurred, if any
14
+ # @!attribute [r] last_result
15
+ # @return [Any] The last result that occurred, if any
16
+ # @!attribute [r] retry_max
17
+ # @return [Integer] The maximum number of workflow runs to attempt before failing. Default: 3
18
+ # @!attribute [r] retry_delay
19
+ # @return [Integer] The number of seconds to wait between workflow runs. Default: 0
20
+ # @!attribute [r] steps
21
+ # @return [Array<Step>] The steps that are part of this workflow
22
+ class Manager
23
+ include CemAcpt::LoggingAsync
24
+
25
+ attr_reader :completed_steps, :last_error, :last_result, :retry_max, :retry_delay, :steps
26
+
27
+ # @param [Hash] opts The options to create a new workflow manager with
28
+ # @option opts [Integer] :retry_max The maximum number of workflow runs to attempt before failing. Default: 3
29
+ # @option opts [Integer] :retry_delay The number of seconds to wait between workflow runs. Default: 0
30
+ # @option opts [Boolean] :ignore_failures Whether to ignore Step failures and continue to the next step. Default: false
31
+ # @option opts [Boolean] :raise_on_fail Whether to raise an exception when a Step fails. Default: true
32
+ def initialize(**opts)
33
+ @retry_max = opts[:retry_max] || 3
34
+ @retry_delay = opts[:retry_delay] || 0
35
+ @ignore_failures = opts[:ignore_failures] || false
36
+ @raise_on_fail = opts[:raise_on_fail] || true
37
+ @steps = []
38
+ @workflow_runs = 0
39
+ @last_error = nil
40
+ @last_result = nil
41
+ @completed_steps = []
42
+ end
43
+
44
+ # Add a step to the workflow. Steps can be named anything, but if the step name is :clean_up,
45
+ # it will be run at the end of the workflow, regardless of the order it was added, and it's
46
+ # output will not be saved as the last_result. Additionally, the :clean_up step will be run
47
+ # even if the workflow fails.
48
+ # @param [Symbol] name The name of the step
49
+ # @param [Hash] kwargs The keyword arguments to pass to the step
50
+ # @param [Proc] block The block to pass to the step
51
+ # @yieldparam [Step] step The step that was added. This is passed to the block.
52
+ def add_step(name, **kwargs, &block)
53
+ raise ArgumentError, 'name must be a Symbol' unless name.is_a?(Symbol)
54
+
55
+ step_name = if @steps.any? { |step| step.name == name }
56
+ "#{name}_#{@steps.length}".to_sym
57
+ else
58
+ name
59
+ end
60
+ step = Step.new(step_name, **kwargs, &block)
61
+ @steps << StepState.new(step, @steps.length, **kwargs)
62
+ end
63
+
64
+ # Run the workflow
65
+ def run
66
+ @workflow_runs += 1
67
+ @completed_steps = []
68
+ @steps.each do |step|
69
+ next if step.name == :clean_up
70
+
71
+ result = step.run
72
+ handle_result(step, result)
73
+ end
74
+ ensure
75
+ clean_up
76
+ end
77
+
78
+ # Whether the workflow has completed successfully
79
+ def success?
80
+ (@completed_steps.length == @steps.length) && @completed_steps.none? { |step| step.failed? }
81
+ end
82
+
83
+ # @return [Boolean] Whether to ignore failures and continue to the next step
84
+ def ignore_failures?
85
+ @ignore_failures
86
+ end
87
+
88
+ # @return [Boolean] Whether to raise an exception when a step fails
89
+ def raise_on_fail?
90
+ @raise_on_fail
91
+ end
92
+
93
+ private
94
+
95
+ # Handles the result of a step
96
+ # @param [Step] step The step that was run
97
+ # @param [Object] result The result of the step
98
+ # @raise [StandardError] If the step failed and the workflow is not configured to ignore failures
99
+ def handle_result(step, result)
100
+ @last_result = result unless step.name == :clean_up # Don't overwrite the last result with the clean_up step
101
+ case result
102
+ when :retry_workflow
103
+ async_warn("Step '#{step.name}' failed and requested a workflow retry")
104
+ async_debug("Step '#{step.name}' failed with error: #{step.last_error.message}")
105
+ async_debug("Step '#{step.name}' failed with error: #{step.last_error.backtrace.join("\n")}")
106
+ @last_error = step.last_error
107
+ retry_workflow
108
+ when :fail
109
+ async_warn("Step '#{step.name}' failed")
110
+ async_debug(step.last_error.message)
111
+ async_debug(step.last_error.backtrace.join("\n"))
112
+ @last_error = step.last_error
113
+ if ignore_failures?
114
+ async_warn("Ignoring failure of step '#{step.name}'")
115
+ @completed_steps << step
116
+ else
117
+ async_error("Workflow failed with error: #{@last_error.message}")
118
+ raise @last_error
119
+ end
120
+ else
121
+ async_info("Step '#{step.name}' succeeded")
122
+ async_debug("Step '#{step.name}' returned: #{result}")
123
+ @completed_steps << step
124
+ end
125
+ end
126
+
127
+ # Retries the workflow if it is retryable
128
+ # @raise [StandardError] If the workflow is not retryable or has exceeded the maximum number of retries
129
+ def retry_workflow
130
+ if @workflow_runs < @retry_max
131
+ async_info("Retrying workflow (attempt #{@workflow_runs + 1} of #{@retry_max})")
132
+ sleep @retry_delay if @retry_delay > 0
133
+ clean_up
134
+ run
135
+ else
136
+ async_fatal('Workflow is not retryable or has exceeded the maximum number of retries')
137
+ raise @last_error
138
+ end
139
+ end
140
+
141
+ # Adds a default clean_up step if one is not defined
142
+ # @return [Step] The default clean_up step
143
+ def default_clean_up
144
+ add_step(:clean_up) do
145
+ async_info('No clean_up step defined, skipping')
146
+ true
147
+ end
148
+ @steps.last
149
+ end
150
+
151
+ # Runs the clean_up step
152
+ def clean_up
153
+ cleanup_step = @steps.find { |step| step.name == :clean_up } || default_clean_up
154
+ result = cleanup_step.run
155
+ handle_result(cleanup_step, result)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../logging'
4
+
5
+ module CemAcpt
6
+ module TestRunner
7
+ module Workflow
8
+ # Error used to wrap fatal errors raised in Runner steps
9
+ # @!attribute [r] step
10
+ # @return [Step] The step that raised the error
11
+ class StepError < StandardError
12
+ attr_reader :step
13
+
14
+ def initialize(step, err)
15
+ @step = step
16
+ @original_error = err
17
+ super err
18
+ set_backtrace err.backtrace if err.respond_to?(:backtrace)
19
+ end
20
+ end
21
+
22
+ # Step is a class that defines a single step in a Workflow.
23
+ # Step objects are created by the Workflow class and are not intended to be created directly.
24
+ # @!attribute [r] name
25
+ # @return [Symbol] The name of the step
26
+ # @!attribute [r] opts
27
+ # @return [Hash] The options passed to the step
28
+ # @!attribute [r] result
29
+ # @return [Object] The result of the step
30
+ class Step
31
+ include CemAcpt::LoggingAsync
32
+
33
+ attr_reader :name, :opts, :result
34
+
35
+ # @param name [Symbol] The name of the step
36
+ # @param opts [Hash] The options passed to the step
37
+ # @param block [Proc] The block to execute when the step is run
38
+ # @yieldparam step [Step] This step object is yielded to the block
39
+ def initialize(name, **opts, &block)
40
+ @name = name
41
+ @opts = opts
42
+ @block = block
43
+ @result = :not_run
44
+ @failed = false
45
+ @log_prefix = opts[:log_prefix] || "#{@name.upcase}:"
46
+ end
47
+
48
+ # @return [Boolean] True if the step has been run and failed
49
+ def failed?
50
+ @failed
51
+ end
52
+
53
+ # Run the step. This calls and executes the block passed to the constructor.
54
+ # @param log_prefix [String] The prefix to use when logging the step name
55
+ # @return [Object] The result of the step
56
+ def run(_log_prefix = 'Running step')
57
+ async_info(log_msg('Starting step'))
58
+ @result = @block.call(self)
59
+ async_debug(log_msg('SUCCESS'))
60
+ @result
61
+ rescue StandardError => e
62
+ async_debug(log_msg("FAILED: #{e.message}"))
63
+ @result = StepError.new(@name, e)
64
+ @failed = true
65
+ @result
66
+ ensure
67
+ Thread.pass # Be kind to the scheduler
68
+ end
69
+
70
+ private
71
+
72
+ def log_msg(msg)
73
+ [@log_prefix, msg].join(' ')
74
+ end
75
+ end
76
+
77
+ # StepState is a class that holds the state of a Step.
78
+ # StepState objects are created by the Workflow class and are not intended to be created directly.
79
+ # @!attribute [r] step
80
+ # @return [Step] The step object
81
+ # @!attribute [r] position
82
+ # @return [Integer] The position of the step in the workflow
83
+ # @!attribute [r] opts
84
+ # @return [Hash] The options passed to the step
85
+ # @!attribute [r] run_count
86
+ # @return [Integer] The number of times the step has been run
87
+ # @!attribute [r] results
88
+ # @return [Array<Object>] The results of all runs of the step
89
+ # @!attribute [r] last_error
90
+ # @return [StepError] The last error raised by the step
91
+ # @!attribute [r] last_result
92
+ # @return [Object] The result of the last run of the step
93
+ class StepState
94
+ attr_reader :step, :position, :opts, :run_count, :results, :last_error, :last_result
95
+
96
+ # @param step [Step] The step object
97
+ # @param position [Integer] The position of the step in the workflow
98
+ # @param opts [Hash] The options passed to the step
99
+ def initialize(step, position, **opts)
100
+ @step = step
101
+ @position = position
102
+ @retryable = opts[:retryable] || false
103
+ @retry_max = opts[:retry_max] || 3
104
+ @retry_delay = opts[:retry_delay] || 0
105
+ @retry_workflow_on_fail = opts[:retry_workflow_on_fail] || false
106
+ @run_count = 0
107
+ @results = []
108
+ @last_error = nil
109
+ @last_result = nil
110
+ end
111
+
112
+ # Proxy any methods not defined in this class to the Step
113
+ def method_missing(method, *args, &block)
114
+ if @step.respond_to?(method)
115
+ @step.send(method, *args, &block)
116
+ else
117
+ super
118
+ end
119
+ end
120
+
121
+ # Proxy any methods not defined in this class to the Step
122
+ def respond_to_missing?(method, include_private = false)
123
+ @step.respond_to?(method) || super
124
+ end
125
+
126
+ # @return [Object] The result of the last run of the step
127
+ def result
128
+ @last_result || :not_run
129
+ end
130
+
131
+ # @return [Boolean] If the step is retryable
132
+ def retryable?
133
+ @retryable
134
+ end
135
+
136
+ # @return [Boolean] If the workflow should be retried if the step fails
137
+ def retry_workflow_on_fail?
138
+ @retry_workflow_on_fail
139
+ end
140
+
141
+ # @return [Boolean] True if the step has been run and failed
142
+ def failed?
143
+ @results.last.is_a?(StepError)
144
+ end
145
+
146
+ # Run the step. This wraps the Step#run method and handles updating the state of the step.
147
+ # @param log_prefix [String] The prefix to use when logging the step name
148
+ # @return [Object] The result of the step
149
+ def run(log_prefix = 'Running step')
150
+ @run_count += 1
151
+ @last_result = @step.run(log_prefix)
152
+ @results << @last_result
153
+ if @last_result.is_a?(StepError)
154
+ handle_error(@last_result)
155
+ end
156
+ @last_result
157
+ end
158
+
159
+ private
160
+
161
+ # Handle the error raised by the step
162
+ # @param result [StepError] The result of the step
163
+ def handle_error(result)
164
+ @last_error = result
165
+ if retry_workflow_on_fail?
166
+ @last_result = :retry_workflow
167
+ elsif retry?
168
+ retry_step
169
+ else
170
+ @last_result = :fail
171
+ end
172
+ end
173
+
174
+ # @return [Boolean] True if the step should be retried
175
+ def retry?
176
+ @retryable && @run_count < @retry_max
177
+ end
178
+
179
+ # Retry running the step
180
+ def retry_step
181
+ sleep @retry_delay if @retry_delay > 0
182
+ run("Retrying step (attempt #{@run_count} of #{@retry_max})")
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end