foreman_openbolt 1.1.0 → 1.2.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 +4 -4
- data/README.md +14 -252
- data/Rakefile +0 -0
- data/app/controllers/foreman_openbolt/task_controller.rb +4 -1
- data/lib/foreman_openbolt/engine.rb +128 -24
- data/lib/foreman_openbolt/version.rb +1 -1
- data/lib/proxy_api/openbolt.rb +13 -3
- data/package.json +1 -1
- data/test/acceptance/acceptance_helper.rb +146 -0
- data/test/acceptance/docker/docker-compose.yml +69 -0
- data/test/acceptance/docker/foreman/Dockerfile +45 -0
- data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
- data/test/acceptance/docker/target/Dockerfile +29 -0
- data/test/acceptance/docker/target/entrypoint.sh +11 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
- data/test/acceptance/fixtures/openbolt.yml +7 -0
- data/test/acceptance/tests/error_handling_test.rb +40 -0
- data/test/acceptance/tests/host_selector_test.rb +31 -0
- data/test/acceptance/tests/launch_task_test.rb +96 -0
- data/test/acceptance/tests/parameter_table_test.rb +61 -0
- data/test/acceptance/tests/settings_test.rb +95 -0
- data/test/acceptance/tests/ssh_options_test.rb +77 -0
- data/test/acceptance/tests/task_execution_test.rb +40 -0
- data/test/acceptance/tests/task_history_test.rb +84 -0
- data/test/acceptance/tests/transport_options_test.rb +161 -0
- data/test/test_plugin_helper.rb +17 -0
- data/test/unit/controllers/task_controller_test.rb +426 -0
- data/test/unit/docker/Dockerfile +47 -0
- data/test/unit/docker/docker-compose.yml +33 -0
- data/test/unit/docker/entrypoint.sh +4 -0
- data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
- data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
- data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
- data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
- data/test/unit/models/task_job_test.rb +278 -0
- data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +45 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +1 -0
- data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +1 -1
- data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +1 -1
- metadata +39 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
class TaskControllerTest < ActionController::TestCase
|
|
6
|
+
tests ForemanOpenbolt::TaskController
|
|
7
|
+
|
|
8
|
+
setup do
|
|
9
|
+
@routes = ForemanOpenbolt::Engine.routes
|
|
10
|
+
@proxy = FactoryBot.create(:smart_proxy)
|
|
11
|
+
@session = set_session_user
|
|
12
|
+
WebMock.reset!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
teardown do
|
|
16
|
+
WebMock.reset!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'fetch_tasks' do
|
|
20
|
+
test 'returns tasks from proxy as JSON' do
|
|
21
|
+
tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
22
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
23
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
24
|
+
|
|
25
|
+
get :fetch_tasks, params: { proxy_id: @proxy.id }, session: @session
|
|
26
|
+
assert_response :success
|
|
27
|
+
assert_equal tasks, JSON.parse(response.body)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test 'returns error when proxy_id is missing' do
|
|
31
|
+
get :fetch_tasks, session: @session
|
|
32
|
+
assert_response :bad_request
|
|
33
|
+
body = JSON.parse(response.body)
|
|
34
|
+
assert_match(/Smart Proxy ID is required/, body['error'])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
test 'returns error when proxy not found' do
|
|
38
|
+
get :fetch_tasks, params: { proxy_id: -1 }, session: @session
|
|
39
|
+
assert_response :not_found
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
test 'returns internal_server_error when proxy is unreachable' do
|
|
43
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks").to_timeout
|
|
44
|
+
|
|
45
|
+
get :fetch_tasks, params: { proxy_id: @proxy.id }, session: @session
|
|
46
|
+
assert_response :internal_server_error
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context 'reload_tasks' do
|
|
51
|
+
test 'returns reloaded tasks from proxy' do
|
|
52
|
+
tasks = { 'new::task' => {} }
|
|
53
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/reload")
|
|
54
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
55
|
+
|
|
56
|
+
get :reload_tasks, params: { proxy_id: @proxy.id }, session: @session
|
|
57
|
+
assert_response :success
|
|
58
|
+
assert_equal tasks, JSON.parse(response.body)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'launch_task' do
|
|
63
|
+
setup do
|
|
64
|
+
@tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
65
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
66
|
+
.to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
67
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
68
|
+
.to_return(status: 200, body: { 'id' => 'launched-job-1' }.to_json,
|
|
69
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
70
|
+
ForemanTasks.stubs(:async_task)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
test 'launches task and returns job_id' do
|
|
74
|
+
post :launch_task, params: {
|
|
75
|
+
proxy_id: @proxy.id,
|
|
76
|
+
task_name: 'mymod::install',
|
|
77
|
+
targets: 'host1.example.com',
|
|
78
|
+
params: { 'name' => 'nginx' },
|
|
79
|
+
options: { 'transport' => 'ssh' },
|
|
80
|
+
}, session: @session
|
|
81
|
+
|
|
82
|
+
assert_response :success
|
|
83
|
+
body = JSON.parse(response.body)
|
|
84
|
+
assert_equal 'launched-job-1', body['job_id']
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
test 'schedules polling after creating task' do
|
|
88
|
+
ForemanTasks.expects(:async_task).with(
|
|
89
|
+
Actions::ForemanOpenbolt::PollTaskStatus,
|
|
90
|
+
'launched-job-1',
|
|
91
|
+
@proxy.id
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
post :launch_task, params: {
|
|
95
|
+
proxy_id: @proxy.id,
|
|
96
|
+
task_name: 'mymod::install',
|
|
97
|
+
targets: 'host1.example.com',
|
|
98
|
+
}, session: @session
|
|
99
|
+
|
|
100
|
+
assert_response :success
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
test 'creates a TaskJob record' do
|
|
104
|
+
assert_difference('ForemanOpenbolt::TaskJob.count', 1) do
|
|
105
|
+
post :launch_task, params: {
|
|
106
|
+
proxy_id: @proxy.id,
|
|
107
|
+
task_name: 'mymod::install',
|
|
108
|
+
targets: 'host1.example.com,host2.example.com',
|
|
109
|
+
}, session: @session
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
job = ForemanOpenbolt::TaskJob.last
|
|
113
|
+
assert_equal 'mymod::install', job.task_name
|
|
114
|
+
assert_equal %w[host1.example.com host2.example.com], job.targets
|
|
115
|
+
assert_equal 'pending', job.status
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test 'returns error when task_name is missing' do
|
|
119
|
+
post :launch_task, params: { proxy_id: @proxy.id, targets: 'host1' }, session: @session
|
|
120
|
+
assert_response :bad_request
|
|
121
|
+
assert_match(/task_name/, JSON.parse(response.body)['error'])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test 'returns error when targets is missing' do
|
|
125
|
+
post :launch_task, params: { proxy_id: @proxy.id, task_name: 'test::task' }, session: @session
|
|
126
|
+
assert_response :bad_request
|
|
127
|
+
assert_match(/targets/, JSON.parse(response.body)['error'])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
test 'returns error when proxy returns error in response' do
|
|
131
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
132
|
+
.to_return(status: 200, body: { 'error' => 'Task not found' }.to_json,
|
|
133
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
134
|
+
|
|
135
|
+
post :launch_task, params: {
|
|
136
|
+
proxy_id: @proxy.id,
|
|
137
|
+
task_name: 'missing::task',
|
|
138
|
+
targets: 'host1',
|
|
139
|
+
}, session: @session
|
|
140
|
+
assert_response :bad_request
|
|
141
|
+
assert_match(/Task execution failed/, JSON.parse(response.body)['error'])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
test 'returns bad_gateway when proxy returns invalid JSON' do
|
|
145
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
146
|
+
.to_return(status: 200, body: 'not valid json',
|
|
147
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
148
|
+
|
|
149
|
+
post :launch_task, params: {
|
|
150
|
+
proxy_id: @proxy.id,
|
|
151
|
+
task_name: 'mymod::install',
|
|
152
|
+
targets: 'host1',
|
|
153
|
+
}, session: @session
|
|
154
|
+
assert_response :bad_gateway
|
|
155
|
+
assert_match(/Smart Proxy error/, JSON.parse(response.body)['error'])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
test 'returns error when proxy returns no job ID' do
|
|
159
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
160
|
+
.to_return(status: 200, body: { 'status' => 'ok' }.to_json,
|
|
161
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
162
|
+
|
|
163
|
+
post :launch_task, params: {
|
|
164
|
+
proxy_id: @proxy.id,
|
|
165
|
+
task_name: 'test::task',
|
|
166
|
+
targets: 'host1',
|
|
167
|
+
}, session: @session
|
|
168
|
+
assert_response :bad_request
|
|
169
|
+
assert_match(/No job ID returned/, JSON.parse(response.body)['error'])
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context 'job_status' do
|
|
174
|
+
test 'returns serialized task job' do
|
|
175
|
+
job = FactoryBot.create(:task_job, :running, smart_proxy: @proxy)
|
|
176
|
+
|
|
177
|
+
get :job_status, params: { job_id: job.job_id }, session: @session
|
|
178
|
+
assert_response :success
|
|
179
|
+
|
|
180
|
+
body = JSON.parse(response.body)
|
|
181
|
+
assert_equal 'running', body['status']
|
|
182
|
+
assert_equal job.task_name, body['task_name']
|
|
183
|
+
assert_equal job.targets, body['targets']
|
|
184
|
+
assert_equal @proxy.id, body['smart_proxy']['id']
|
|
185
|
+
assert_equal @proxy.name, body['smart_proxy']['name']
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
test 'returns error when job_id is missing' do
|
|
189
|
+
get :job_status, session: @session
|
|
190
|
+
assert_response :bad_request
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
test 'returns not_found when job does not exist' do
|
|
194
|
+
get :job_status, params: { job_id: 'nonexistent' }, session: @session
|
|
195
|
+
assert_response :not_found
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
context 'job_result' do
|
|
200
|
+
test 'returns result and log for completed job' do
|
|
201
|
+
ForemanTasks.stubs(:async_task)
|
|
202
|
+
job = FactoryBot.create(:task_job, :success, smart_proxy: @proxy)
|
|
203
|
+
|
|
204
|
+
get :job_result, params: { job_id: job.job_id }, session: @session
|
|
205
|
+
assert_response :success
|
|
206
|
+
|
|
207
|
+
body = JSON.parse(response.body)
|
|
208
|
+
assert_equal 'success', body['status']
|
|
209
|
+
assert_equal job.result, body['value']
|
|
210
|
+
assert_equal job.log, body['log']
|
|
211
|
+
assert_equal job.command, body['command']
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
test 'returns not_found when job does not exist' do
|
|
215
|
+
get :job_result, params: { job_id: 'nonexistent' }, session: @session
|
|
216
|
+
assert_response :not_found
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
context 'launch_task with encrypted options' do
|
|
221
|
+
setup do
|
|
222
|
+
@tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
223
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
224
|
+
.to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
225
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
226
|
+
.to_return(status: 200, body: { 'id' => 'encrypted-job-1' }.to_json,
|
|
227
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
228
|
+
ForemanTasks.stubs(:async_task)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
test 'sends real encrypted value to proxy and scrubs it in database' do
|
|
232
|
+
Setting['openbolt_password'] = 'real-secret-password'
|
|
233
|
+
|
|
234
|
+
post :launch_task, params: {
|
|
235
|
+
proxy_id: @proxy.id,
|
|
236
|
+
task_name: 'mymod::install',
|
|
237
|
+
targets: 'host1.example.com',
|
|
238
|
+
options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
|
|
239
|
+
}, session: @session
|
|
240
|
+
|
|
241
|
+
assert_response :success
|
|
242
|
+
|
|
243
|
+
assert_requested(:post, "#{@proxy.url}/openbolt/launch/task") do |req|
|
|
244
|
+
sent_body = JSON.parse(req.body)
|
|
245
|
+
sent_body['options']['password'] == 'real-secret-password'
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
|
|
249
|
+
assert_equal '*****', job.openbolt_options['password']
|
|
250
|
+
assert_equal 'ssh', job.openbolt_options['transport']
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
test 'returns error when encrypted placeholder used for nonexistent setting' do
|
|
254
|
+
post :launch_task, params: {
|
|
255
|
+
proxy_id: @proxy.id,
|
|
256
|
+
task_name: 'mymod::install',
|
|
257
|
+
targets: 'host1.example.com',
|
|
258
|
+
options: { 'nonexistent-option' => '[Use saved encrypted default]' },
|
|
259
|
+
}, session: @session
|
|
260
|
+
|
|
261
|
+
assert_response :bad_request
|
|
262
|
+
assert_match(/No saved value for encrypted option/, JSON.parse(response.body)['error'])
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
test 'passes non-encrypted options through unchanged' do
|
|
266
|
+
post :launch_task, params: {
|
|
267
|
+
proxy_id: @proxy.id,
|
|
268
|
+
task_name: 'mymod::install',
|
|
269
|
+
targets: 'host1.example.com',
|
|
270
|
+
options: { 'transport' => 'ssh', 'user' => 'admin' },
|
|
271
|
+
}, session: @session
|
|
272
|
+
|
|
273
|
+
assert_response :success
|
|
274
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
|
|
275
|
+
assert_equal 'ssh', job.openbolt_options['transport']
|
|
276
|
+
assert_equal 'admin', job.openbolt_options['user']
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
context 'fetch_openbolt_options with settings defaults' do
|
|
281
|
+
test 'merges Foreman setting defaults into proxy options' do
|
|
282
|
+
Setting['openbolt_transport'] = 'winrm'
|
|
283
|
+
Setting['openbolt_user'] = 'admin'
|
|
284
|
+
|
|
285
|
+
proxy_options = {
|
|
286
|
+
'transport' => { 'type' => %w[ssh winrm] },
|
|
287
|
+
'user' => { 'type' => 'string' },
|
|
288
|
+
'verbose' => { 'type' => 'boolean' },
|
|
289
|
+
}
|
|
290
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
291
|
+
.to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
292
|
+
|
|
293
|
+
get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
|
|
294
|
+
assert_response :success
|
|
295
|
+
|
|
296
|
+
body = JSON.parse(response.body)
|
|
297
|
+
assert_equal 'winrm', body['transport']['default']
|
|
298
|
+
assert_equal 'admin', body['user']['default']
|
|
299
|
+
assert_equal false, body['verbose']['default']
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
test 'shows encrypted placeholder instead of real value for encrypted settings' do
|
|
303
|
+
Setting['openbolt_password'] = 'secret-value'
|
|
304
|
+
|
|
305
|
+
proxy_options = {
|
|
306
|
+
'password' => { 'type' => 'string', 'sensitive' => true },
|
|
307
|
+
}
|
|
308
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
309
|
+
.to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
310
|
+
|
|
311
|
+
get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
|
|
312
|
+
assert_response :success
|
|
313
|
+
|
|
314
|
+
body = JSON.parse(response.body)
|
|
315
|
+
assert_equal '[Use saved encrypted default]', body['password']['default']
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
context 'fetch_openbolt_options with Choria settings defaults' do
|
|
320
|
+
# Mirrors the real GET /openbolt/tasks/options response for Choria
|
|
321
|
+
# (see smart_proxy_openbolt/lib/smart_proxy_openbolt/main.rb OPENBOLT_OPTIONS).
|
|
322
|
+
def self.choria_proxy_options
|
|
323
|
+
{
|
|
324
|
+
'choria-task-agent' => { 'type' => %w[bolt_tasks shell], 'transport' => ['choria'], 'sensitive' => false },
|
|
325
|
+
'choria-config-file' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
326
|
+
'choria-mcollective-certname' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
327
|
+
'choria-ssl-ca' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
328
|
+
'choria-ssl-cert' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
329
|
+
'choria-ssl-key' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
330
|
+
'choria-collective' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
331
|
+
'choria-puppet-environment' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
332
|
+
'choria-rpc-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
333
|
+
'choria-task-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
334
|
+
'choria-command-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
335
|
+
'choria-brokers' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
336
|
+
'choria-broker-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
test 'merges all Choria setting values into their option defaults' do
|
|
341
|
+
Setting['openbolt_choria-task-agent'] = 'shell'
|
|
342
|
+
Setting['openbolt_choria-config-file'] = '/etc/choria/client.conf'
|
|
343
|
+
Setting['openbolt_choria-mcollective-certname'] = 'primary.example.com'
|
|
344
|
+
Setting['openbolt_choria-ssl-ca'] = '/etc/choria/ca.pem'
|
|
345
|
+
Setting['openbolt_choria-ssl-cert'] = '/etc/choria/client.pem'
|
|
346
|
+
Setting['openbolt_choria-ssl-key'] = '/etc/choria/client.key'
|
|
347
|
+
Setting['openbolt_choria-collective'] = 'mcollective'
|
|
348
|
+
Setting['openbolt_choria-puppet-environment'] = 'production'
|
|
349
|
+
Setting['openbolt_choria-rpc-timeout'] = 60
|
|
350
|
+
Setting['openbolt_choria-task-timeout'] = 300
|
|
351
|
+
Setting['openbolt_choria-command-timeout'] = 120
|
|
352
|
+
Setting['openbolt_choria-brokers'] = 'broker.example.com:4222'
|
|
353
|
+
Setting['openbolt_choria-broker-timeout'] = 10
|
|
354
|
+
|
|
355
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
356
|
+
.to_return(status: 200, body: self.class.choria_proxy_options.to_json,
|
|
357
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
358
|
+
|
|
359
|
+
get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
|
|
360
|
+
assert_response :success
|
|
361
|
+
|
|
362
|
+
body = JSON.parse(response.body)
|
|
363
|
+
assert_equal 'shell', body['choria-task-agent']['default']
|
|
364
|
+
assert_equal '/etc/choria/client.conf', body['choria-config-file']['default']
|
|
365
|
+
assert_equal 'primary.example.com', body['choria-mcollective-certname']['default']
|
|
366
|
+
assert_equal '/etc/choria/ca.pem', body['choria-ssl-ca']['default']
|
|
367
|
+
assert_equal '/etc/choria/client.pem', body['choria-ssl-cert']['default']
|
|
368
|
+
assert_equal '/etc/choria/client.key', body['choria-ssl-key']['default']
|
|
369
|
+
assert_equal 'mcollective', body['choria-collective']['default']
|
|
370
|
+
assert_equal 'production', body['choria-puppet-environment']['default']
|
|
371
|
+
assert_equal 60, body['choria-rpc-timeout']['default']
|
|
372
|
+
assert_equal 300, body['choria-task-timeout']['default']
|
|
373
|
+
assert_equal 120, body['choria-command-timeout']['default']
|
|
374
|
+
assert_equal 'broker.example.com:4222', body['choria-brokers']['default']
|
|
375
|
+
assert_equal 10, body['choria-broker-timeout']['default']
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
test 'omits nil-default settings and keeps real defaults when Choria settings are not configured' do
|
|
379
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
380
|
+
.to_return(status: 200, body: self.class.choria_proxy_options.to_json,
|
|
381
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
382
|
+
|
|
383
|
+
get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
|
|
384
|
+
assert_response :success
|
|
385
|
+
|
|
386
|
+
body = JSON.parse(response.body)
|
|
387
|
+
assert_equal 'bolt_tasks', body['choria-task-agent']['default']
|
|
388
|
+
assert_not body['choria-config-file'].key?('default')
|
|
389
|
+
assert_not body['choria-mcollective-certname'].key?('default')
|
|
390
|
+
assert_not body['choria-ssl-key'].key?('default')
|
|
391
|
+
assert_not body['choria-brokers'].key?('default')
|
|
392
|
+
assert_not body['choria-broker-timeout'].key?('default')
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
context 'fetch_task_history' do
|
|
397
|
+
test 'returns paginated task history' do
|
|
398
|
+
3.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
|
|
399
|
+
|
|
400
|
+
get :fetch_task_history, params: { page: 1, per_page: 2 }, session: @session
|
|
401
|
+
assert_response :success
|
|
402
|
+
|
|
403
|
+
body = JSON.parse(response.body)
|
|
404
|
+
assert_equal 2, body['results'].length
|
|
405
|
+
assert_equal 3, body['total']
|
|
406
|
+
assert_equal 1, body['page']
|
|
407
|
+
assert_equal 2, body['per_page']
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
test 'caps per_page at 100' do
|
|
411
|
+
get :fetch_task_history, params: { per_page: 200 }, session: @session
|
|
412
|
+
assert_response :success
|
|
413
|
+
|
|
414
|
+
body = JSON.parse(response.body)
|
|
415
|
+
assert_equal 100, body['per_page']
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
test 'defaults per_page to 20' do
|
|
419
|
+
get :fetch_task_history, session: @session
|
|
420
|
+
assert_response :success
|
|
421
|
+
|
|
422
|
+
body = JSON.parse(response.body)
|
|
423
|
+
assert_equal 20, body['per_page']
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Base test image: Foreman + system deps on Rocky Linux 9.
|
|
2
|
+
# This image is stable for a given Foreman version and does not
|
|
3
|
+
# depend on any foreman_openbolt code. All plugin wiring happens
|
|
4
|
+
# at runtime via the rake test:up task.
|
|
5
|
+
#
|
|
6
|
+
# Build:
|
|
7
|
+
# FOREMAN_BRANCH=3.18-stable docker compose -f test/unit/docker/docker-compose.yml build test
|
|
8
|
+
|
|
9
|
+
FROM rockylinux:9
|
|
10
|
+
|
|
11
|
+
# System packages -- cached across all Foreman versions.
|
|
12
|
+
RUN dnf module enable -y nodejs:22 postgresql:16 && \
|
|
13
|
+
dnf install -y \
|
|
14
|
+
ruby ruby-devel rubygem-bundler \
|
|
15
|
+
gcc gcc-c++ make \
|
|
16
|
+
postgresql-devel postgresql \
|
|
17
|
+
libffi-devel \
|
|
18
|
+
nodejs npm \
|
|
19
|
+
git-core \
|
|
20
|
+
redhat-rpm-config && \
|
|
21
|
+
dnf clean all
|
|
22
|
+
|
|
23
|
+
ARG FOREMAN_BRANCH=3.18-stable
|
|
24
|
+
|
|
25
|
+
# Clone Foreman at the specified branch.
|
|
26
|
+
RUN git init /opt/foreman
|
|
27
|
+
|
|
28
|
+
WORKDIR /opt/foreman
|
|
29
|
+
|
|
30
|
+
RUN git remote add origin https://github.com/theforeman/foreman.git && \
|
|
31
|
+
git fetch --depth=1 origin ${FOREMAN_BRANCH} && \
|
|
32
|
+
git checkout FETCH_HEAD
|
|
33
|
+
|
|
34
|
+
# Configure database for the test container.
|
|
35
|
+
RUN printf "test:\n adapter: postgresql\n host: db\n port: 5432\n username: foreman\n password: foreman\n database: foreman_test\n encoding: utf8\n pool: 5\n" \
|
|
36
|
+
> config/database.yml
|
|
37
|
+
|
|
38
|
+
# Install Foreman's own gems (without the plugin). This is the
|
|
39
|
+
# expensive layer but only changes when the Foreman version changes.
|
|
40
|
+
ENV BUNDLE_WITHOUT="release console journald libvirt ec2 openstack vmware telemetry"
|
|
41
|
+
ENV BUNDLE_PATH="/opt/bundle"
|
|
42
|
+
RUN bundle install --jobs=4
|
|
43
|
+
|
|
44
|
+
COPY test/unit/docker/entrypoint.sh /entrypoint.sh
|
|
45
|
+
RUN chmod +x /entrypoint.sh
|
|
46
|
+
|
|
47
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: foreman-openbolt-unit
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
db:
|
|
5
|
+
image: postgres:16-alpine
|
|
6
|
+
environment:
|
|
7
|
+
POSTGRES_USER: foreman
|
|
8
|
+
POSTGRES_PASSWORD: foreman
|
|
9
|
+
POSTGRES_DB: foreman_test
|
|
10
|
+
tmpfs: /var/lib/postgresql/data
|
|
11
|
+
healthcheck:
|
|
12
|
+
test: ["CMD-SHELL", "pg_isready -U foreman"]
|
|
13
|
+
interval: 2s
|
|
14
|
+
timeout: 5s
|
|
15
|
+
retries: 10
|
|
16
|
+
|
|
17
|
+
test:
|
|
18
|
+
image: foreman-openbolt-test:${FOREMAN_BRANCH:-3.18-stable}
|
|
19
|
+
build:
|
|
20
|
+
context: ../../..
|
|
21
|
+
dockerfile: test/unit/docker/Dockerfile
|
|
22
|
+
args:
|
|
23
|
+
FOREMAN_BRANCH: ${FOREMAN_BRANCH:-3.18-stable}
|
|
24
|
+
depends_on:
|
|
25
|
+
db:
|
|
26
|
+
condition: service_healthy
|
|
27
|
+
environment:
|
|
28
|
+
RAILS_ENV: test
|
|
29
|
+
DATABASE_URL: postgresql://foreman:foreman@db:5432/foreman_test
|
|
30
|
+
volumes:
|
|
31
|
+
- ../../..:/opt/foreman_openbolt
|
|
32
|
+
working_dir: /opt/foreman
|
|
33
|
+
command: ["tail", "-f", "/dev/null"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FactoryBot.define do
|
|
4
|
+
factory :task_job, class: 'ForemanOpenbolt::TaskJob' do
|
|
5
|
+
sequence(:job_id) { |n| "test-job-#{n}" }
|
|
6
|
+
association :smart_proxy
|
|
7
|
+
task_name { 'mymod::install' }
|
|
8
|
+
task_description { 'Install a package on the target host' }
|
|
9
|
+
status { 'pending' }
|
|
10
|
+
targets { ['host1.example.com', 'host2.example.com'] }
|
|
11
|
+
submitted_at { Time.current }
|
|
12
|
+
task_parameters { { 'name' => 'nginx' } }
|
|
13
|
+
openbolt_options { { 'transport' => 'ssh' } }
|
|
14
|
+
|
|
15
|
+
trait :running do
|
|
16
|
+
status { 'running' }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
trait :success do
|
|
20
|
+
status { 'success' }
|
|
21
|
+
completed_at { Time.current }
|
|
22
|
+
result { { 'items' => [{ 'target' => 'host1.example.com', 'status' => 'success' }] } }
|
|
23
|
+
log { 'Started: task mymod::install\nFinished: success' }
|
|
24
|
+
command { 'bolt task run mymod::install --targets host1.example.com' }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
trait :failure do
|
|
28
|
+
status { 'failure' }
|
|
29
|
+
completed_at { Time.current }
|
|
30
|
+
result { { 'items' => [{ 'target' => 'host1.example.com', 'status' => 'failure' }] } }
|
|
31
|
+
log { 'Started: task mymod::install\nFinished: failure' }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
trait :exception do
|
|
35
|
+
status { 'exception' }
|
|
36
|
+
completed_at { Time.current }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
class CleanupProxyArtifactsTest < ForemanOpenbolt::PluginTestCase
|
|
6
|
+
include Dynflow::Testing
|
|
7
|
+
|
|
8
|
+
context 'plan' do
|
|
9
|
+
test 'stores proxy_id and job_id in input' do
|
|
10
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, 42, 'job-123')
|
|
11
|
+
assert_equal 42, action.input[:proxy_id]
|
|
12
|
+
assert_equal 'job-123', action.input[:job_id]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context 'rescue_strategy' do
|
|
17
|
+
test 'uses Skip rescue strategy' do
|
|
18
|
+
action = create_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts)
|
|
19
|
+
assert_equal Dynflow::Action::Rescue::Skip, action.rescue_strategy
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context 'run' do
|
|
24
|
+
test 'calls delete_job_artifacts when proxy exists' do
|
|
25
|
+
proxy = FactoryBot.create(:smart_proxy)
|
|
26
|
+
stub = stub_request(:delete, "#{proxy.url}/openbolt/job/job-456/artifacts")
|
|
27
|
+
.to_return(status: 200, body: '{"deleted": true}', headers: { 'Content-Type' => 'application/json' })
|
|
28
|
+
|
|
29
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, proxy.id, 'job-456')
|
|
30
|
+
run_action(action)
|
|
31
|
+
|
|
32
|
+
assert_requested(stub)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test 'does not call API when proxy not found' do
|
|
36
|
+
stub = stub_request(:delete, %r{openbolt/job})
|
|
37
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, -1, 'job-456')
|
|
38
|
+
run_action(action)
|
|
39
|
+
assert_not_requested(stub)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
test 'does not raise when API call fails' do
|
|
43
|
+
proxy = FactoryBot.create(:smart_proxy)
|
|
44
|
+
stub_request(:delete, "#{proxy.url}/openbolt/job/job-456/artifacts")
|
|
45
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
46
|
+
|
|
47
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, proxy.id, 'job-456')
|
|
48
|
+
assert_nothing_raised { run_action(action) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|