foreman_openbolt 1.0.0 → 1.1.1
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 +190 -19
- data/Rakefile +17 -93
- data/app/controllers/foreman_openbolt/task_controller.rb +61 -49
- data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +11 -10
- data/app/lib/actions/foreman_openbolt/poll_task_status.rb +70 -60
- data/app/models/foreman_openbolt/task_job.rb +16 -17
- data/config/routes.rb +0 -1
- data/lib/foreman_openbolt/engine.rb +11 -11
- data/lib/foreman_openbolt/version.rb +1 -1
- data/lib/proxy_api/openbolt.rb +25 -9
- data/lib/tasks/foreman_openbolt_tasks.rake +1 -22
- data/locale/gemspec.rb +1 -1
- data/package.json +11 -15
- 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 +121 -0
- data/test/test_plugin_helper.rb +12 -3
- data/test/unit/controllers/task_controller_test.rb +351 -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/__mocks__/foremanReact/common/I18n.js +15 -0
- data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +6 -0
- data/webpack/__mocks__/foremanReact/redux/API/index.js +11 -0
- data/webpack/src/Components/LaunchTask/FieldTable.js +8 -5
- data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +74 -62
- data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +11 -13
- data/webpack/src/Components/LaunchTask/HostSelector/index.js +28 -33
- data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +3 -2
- data/webpack/src/Components/LaunchTask/ParameterField.js +2 -0
- data/webpack/src/Components/LaunchTask/SmartProxySelect.js +2 -1
- data/webpack/src/Components/LaunchTask/TaskSelect.js +3 -3
- data/webpack/src/Components/LaunchTask/__tests__/EmptyContent.test.js +10 -0
- data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +83 -0
- data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +86 -0
- data/webpack/src/Components/LaunchTask/__tests__/ParametersSection.test.js +50 -0
- data/webpack/src/Components/LaunchTask/__tests__/SmartProxySelect.test.js +63 -0
- data/webpack/src/Components/LaunchTask/__tests__/TaskSelect.test.js +39 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +90 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useSmartProxies.test.js +69 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +103 -0
- data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +9 -11
- data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +12 -13
- data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +6 -13
- data/webpack/src/Components/LaunchTask/index.js +9 -27
- data/webpack/src/Components/TaskExecution/ExecutionDetails.js +29 -29
- data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +9 -10
- data/webpack/src/Components/TaskExecution/LoadingIndicator.js +7 -2
- data/webpack/src/Components/TaskExecution/ResultDisplay.js +13 -17
- data/webpack/src/Components/TaskExecution/TaskDetails.js +58 -67
- data/webpack/src/Components/TaskExecution/__tests__/ExecutionDetails.test.js +47 -0
- data/webpack/src/Components/TaskExecution/__tests__/ExecutionDisplay.test.js +29 -0
- data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +25 -0
- data/webpack/src/Components/TaskExecution/__tests__/ResultDisplay.test.js +28 -0
- data/webpack/src/Components/TaskExecution/__tests__/TaskDetails.test.js +38 -0
- data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +80 -0
- data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +177 -0
- data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +34 -33
- data/webpack/src/Components/TaskExecution/index.js +10 -12
- data/webpack/src/Components/TaskHistory/TaskPopover.js +9 -12
- data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +109 -0
- data/webpack/src/Components/TaskHistory/__tests__/TaskPopover.test.js +26 -0
- data/webpack/src/Components/TaskHistory/index.js +21 -29
- data/webpack/src/Components/common/HostsPopover.js +12 -3
- data/webpack/src/Components/common/__tests__/HostsPopover.test.js +20 -0
- data/webpack/src/Components/common/__tests__/helpers.test.js +135 -0
- data/webpack/src/Components/common/helpers.js +34 -5
- data/webpack/test_setup.js +34 -11
- metadata +65 -87
- data/test/factories/foreman_openbolt_factories.rb +0 -7
- data/test/unit/foreman_openbolt_test.rb +0 -13
- data/webpack/global_test_setup.js +0 -11
- data/webpack/webpack.config.js +0 -7
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
module Actions
|
|
7
7
|
module ForemanOpenbolt
|
|
8
8
|
class PollTaskStatus < Actions::EntryAction
|
|
9
|
-
include Actions::RecurringAction
|
|
10
|
-
|
|
11
9
|
POLL_INTERVAL = 5.seconds
|
|
12
|
-
RETRY_LIMIT = 60 #
|
|
10
|
+
RETRY_LIMIT = 60 # 5 minutes at 5-second intervals
|
|
13
11
|
|
|
14
12
|
# Set up the action when it is first scheduled, storing
|
|
15
13
|
# IDs needed to get information from the proxy.
|
|
@@ -24,52 +22,54 @@ module Actions
|
|
|
24
22
|
if event.nil? || event.to_sym == :poll
|
|
25
23
|
poll_and_reschedule
|
|
26
24
|
else
|
|
27
|
-
|
|
25
|
+
log("Received unknown event '#{event}' for OpenBolt job #{input[:job_id]}", :error)
|
|
28
26
|
finish
|
|
29
27
|
end
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
def
|
|
33
|
-
|
|
30
|
+
def log(msg, level = :debug)
|
|
31
|
+
output[:log] ||= []
|
|
32
|
+
output[:log] << "[#{Time.now.getlocal.strftime('%Y-%m-%d %H:%M:%S')}] [#{level.upcase}] #{msg}"
|
|
33
|
+
Rails.logger.send(level, msg)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
output[key] ||= []
|
|
40
|
-
output[key] << message
|
|
36
|
+
def exception(msg, e)
|
|
37
|
+
log("#{msg}: #{e.class}: #{e.message}", :error)
|
|
38
|
+
log(e.backtrace.join("\n"), :error) if e.backtrace
|
|
41
39
|
end
|
|
42
40
|
|
|
43
|
-
def
|
|
44
|
-
|
|
41
|
+
def finish
|
|
42
|
+
log("Polling finished for OpenBolt job #{input[:job_id]}")
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
def extract_proxy_error(response)
|
|
46
|
+
error_value = response&.dig('error')
|
|
47
|
+
return nil if error_value.nil? || (error_value.respond_to?(:empty?) && error_value.empty?)
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
append_output(:exception, e.message)
|
|
53
|
-
append_output(:exception_backtrace, e.backtrace.join("\n"))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def finish
|
|
57
|
-
log("Polling finished for OpenBolt job #{input[:job_id]}")
|
|
49
|
+
error_value.is_a?(Hash) ? error_value['message'] || error_value.to_s : error_value.to_s
|
|
58
50
|
end
|
|
59
51
|
|
|
60
52
|
def poll_and_reschedule
|
|
61
53
|
job_id = input[:job_id]
|
|
54
|
+
task_job = ::ForemanOpenbolt::TaskJob.find_by(job_id: job_id)
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
if task_job.nil?
|
|
57
|
+
log("TaskJob record not found for job #{job_id}", :error)
|
|
58
|
+
finish
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if task_job.completed?
|
|
65
63
|
finish
|
|
66
64
|
return
|
|
67
65
|
end
|
|
68
66
|
|
|
69
67
|
# If the smart proxy has been deleted somehow or is unknown,
|
|
70
68
|
# we can't poll for status, so finish.
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
proxy = ::SmartProxy.find_by(id: input[:proxy_id])
|
|
70
|
+
unless proxy
|
|
71
|
+
log("Smart Proxy with ID #{input[:proxy_id]} not found for OpenBolt job #{job_id}", :error)
|
|
72
|
+
task_job.update!(status: 'exception')
|
|
73
73
|
finish
|
|
74
74
|
return
|
|
75
75
|
end
|
|
@@ -80,24 +80,42 @@ module Actions
|
|
|
80
80
|
# Fetch current status
|
|
81
81
|
status_result = api.job_status(job_id: job_id)
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
task_job.update!(status:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
proxy_error = extract_proxy_error(status_result)
|
|
84
|
+
if proxy_error
|
|
85
|
+
log("Proxy returned error for job #{job_id}: #{proxy_error}", :error)
|
|
86
|
+
task_job.update!(status: 'exception')
|
|
87
|
+
finish
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
unless status_result&.dig('status')
|
|
92
|
+
log("Proxy returned response without status for job #{job_id}: #{status_result.inspect}", :error)
|
|
93
|
+
task_job.update!(status: 'exception')
|
|
94
|
+
finish
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
input[:retry_count] = 0
|
|
99
|
+
new_status = status_result['status']
|
|
100
|
+
if new_status == task_job.status
|
|
101
|
+
log("Poll for OpenBolt job #{job_id}: status=#{new_status}")
|
|
102
|
+
else
|
|
103
|
+
previous_status = task_job.status
|
|
104
|
+
task_job.update!(status: new_status)
|
|
105
|
+
log("OpenBolt job #{job_id} status changed from '#{previous_status}' to '#{new_status}'", :info)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# If completed, fetch full results
|
|
109
|
+
if task_job.completed?
|
|
110
|
+
result = api.job_result(job_id: job_id)
|
|
111
|
+
if result.present?
|
|
112
|
+
task_job.update_from_proxy_result!(result)
|
|
113
|
+
log("OpenBolt job #{job_id} completed with status '#{task_job.status}'", :info)
|
|
114
|
+
else
|
|
115
|
+
log("No result returned from proxy for completed OpenBolt job #{job_id}", :error)
|
|
100
116
|
end
|
|
117
|
+
finish
|
|
118
|
+
return
|
|
101
119
|
end
|
|
102
120
|
|
|
103
121
|
# Still running, schedule next poll in 5 seconds
|
|
@@ -105,14 +123,14 @@ module Actions
|
|
|
105
123
|
world.clock.ping(suspended_action, POLL_INTERVAL.from_now.to_time, :poll)
|
|
106
124
|
end
|
|
107
125
|
rescue StandardError => e
|
|
108
|
-
|
|
109
|
-
exception(e)
|
|
126
|
+
exception("Error polling task status for job #{job_id}", e)
|
|
110
127
|
|
|
111
128
|
retry_count = (input[:retry_count] || 0) + 1
|
|
112
129
|
input[:retry_count] = retry_count
|
|
113
130
|
|
|
114
131
|
if retry_count > RETRY_LIMIT
|
|
115
|
-
|
|
132
|
+
log("Polling gave up for job #{job_id} after #{retry_count} attempts", :error)
|
|
133
|
+
task_job.update!(status: 'exception')
|
|
116
134
|
finish
|
|
117
135
|
return
|
|
118
136
|
end
|
|
@@ -132,19 +150,11 @@ module Actions
|
|
|
132
150
|
end
|
|
133
151
|
|
|
134
152
|
def humanized_input
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def task_job
|
|
143
|
-
@task_job ||= ::ForemanOpenbolt::TaskJob.find_by(job_id: input[:job_id])
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def proxy
|
|
147
|
-
@proxy ||= ::SmartProxy.find_by(id: input[:proxy_id])
|
|
153
|
+
proxy_name = ::SmartProxy.find_by(id: input[:proxy_id])&.name || '(unknown)'
|
|
154
|
+
task_name = ::ForemanOpenbolt::TaskJob.find_by(job_id: input[:job_id])&.task_name
|
|
155
|
+
parts = ["job #{input[:job_id]} on #{proxy_name}"]
|
|
156
|
+
parts << "task: #{task_name}" if task_name
|
|
157
|
+
parts.join(', ')
|
|
148
158
|
end
|
|
149
159
|
end
|
|
150
160
|
end
|
|
@@ -18,7 +18,6 @@ module ForemanOpenbolt
|
|
|
18
18
|
validates :job_id, presence: true, uniqueness: true
|
|
19
19
|
validates :status, inclusion: { in: STATUSES }
|
|
20
20
|
validates :targets, presence: true
|
|
21
|
-
validates :submitted_at, presence: true
|
|
22
21
|
|
|
23
22
|
# Scopes
|
|
24
23
|
scope :running, -> { where(status: RUNNING_STATUSES) }
|
|
@@ -27,7 +26,6 @@ module ForemanOpenbolt
|
|
|
27
26
|
scope :for_proxy, ->(proxy) { where(smart_proxy: proxy) }
|
|
28
27
|
|
|
29
28
|
# Callbacks
|
|
30
|
-
before_validation :set_submitted_at, on: :create
|
|
31
29
|
before_update :set_completed_at, if: :status_changed_to_completed?
|
|
32
30
|
after_update :cleanup_proxy_artifacts, if: :saved_result_and_log?
|
|
33
31
|
|
|
@@ -41,8 +39,8 @@ module ForemanOpenbolt
|
|
|
41
39
|
targets: targets,
|
|
42
40
|
task_parameters: parameters,
|
|
43
41
|
openbolt_options: options,
|
|
44
|
-
status: 'pending'
|
|
45
|
-
|
|
42
|
+
status: 'pending',
|
|
43
|
+
submitted_at: Time.current
|
|
46
44
|
)
|
|
47
45
|
end
|
|
48
46
|
|
|
@@ -61,7 +59,10 @@ module ForemanOpenbolt
|
|
|
61
59
|
|
|
62
60
|
# Result/log will already be scrubbed by the proxy
|
|
63
61
|
def update_from_proxy_result!(proxy_result)
|
|
64
|
-
|
|
62
|
+
if proxy_result.blank?
|
|
63
|
+
Rails.logger.warn("Received blank proxy result for job #{job_id}, skipping update")
|
|
64
|
+
return
|
|
65
|
+
end
|
|
65
66
|
|
|
66
67
|
transaction do
|
|
67
68
|
self.status = proxy_result['status'] if proxy_result['status'].present?
|
|
@@ -80,12 +81,6 @@ module ForemanOpenbolt
|
|
|
80
81
|
targets&.join(', ') || ''
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
private
|
|
84
|
-
|
|
85
|
-
def set_submitted_at
|
|
86
|
-
self.submitted_at ||= Time.current
|
|
87
|
-
end
|
|
88
|
-
|
|
89
84
|
def set_completed_at
|
|
90
85
|
self.completed_at ||= Time.current if completed?
|
|
91
86
|
end
|
|
@@ -98,13 +93,17 @@ module ForemanOpenbolt
|
|
|
98
93
|
saved_change_to_result? || saved_change_to_log?
|
|
99
94
|
end
|
|
100
95
|
|
|
96
|
+
# Schedule cleanup of proxy artifacts if we have successfully saved the results
|
|
101
97
|
def cleanup_proxy_artifacts
|
|
102
|
-
return unless
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
return unless completed?
|
|
99
|
+
|
|
100
|
+
ForemanTasks.async_task(::Actions::ForemanOpenbolt::CleanupProxyArtifacts,
|
|
101
|
+
smart_proxy_id,
|
|
102
|
+
job_id)
|
|
103
|
+
Rails.logger.debug { "Scheduled cleanup for job #{job_id} on proxy #{smart_proxy_id}" }
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Rails.logger.error("Failed to schedule cleanup for job #{job_id}: #{e.class}: #{e.message}")
|
|
106
|
+
Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
|
|
108
107
|
end
|
|
109
108
|
end
|
|
110
109
|
end
|
data/config/routes.rb
CHANGED
|
@@ -15,7 +15,7 @@ module ForemanOpenbolt
|
|
|
15
15
|
initializer 'foreman_openbolt.register_plugin', before: :finisher_hook do |app|
|
|
16
16
|
app.reloader.to_prepare do
|
|
17
17
|
Foreman::Plugin.register :foreman_openbolt do
|
|
18
|
-
requires_foreman '>= 3.
|
|
18
|
+
requires_foreman '>= 3.17.0'
|
|
19
19
|
register_gettext
|
|
20
20
|
|
|
21
21
|
settings do
|
|
@@ -33,15 +33,15 @@ module ForemanOpenbolt
|
|
|
33
33
|
|
|
34
34
|
# rubocop:disable Lint/ConstantDefinitionInBlock
|
|
35
35
|
TRANSPORTS = {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
ssh: N_('SSH'),
|
|
37
|
+
winrm: N_('WinRM'),
|
|
38
38
|
}.freeze
|
|
39
39
|
LOG_LEVELS = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
error: N_('Error'),
|
|
41
|
+
warning: N_('Warning'),
|
|
42
|
+
info: N_('Info'),
|
|
43
|
+
debug: N_('Debug'),
|
|
44
|
+
trace: N_('Trace'),
|
|
45
45
|
}.freeze
|
|
46
46
|
# rubocop:enable Lint/ConstantDefinitionInBlock
|
|
47
47
|
|
|
@@ -62,7 +62,7 @@ module ForemanOpenbolt
|
|
|
62
62
|
default: false,
|
|
63
63
|
full_name: N_('Verbose'),
|
|
64
64
|
description: N_(
|
|
65
|
-
'Run the OpenBolt command with the --verbose flag. This prints additional information '
|
|
65
|
+
'Run the OpenBolt command with the --verbose flag. This prints additional information ' \
|
|
66
66
|
'during OpenBolt execution and will print any out::verbose plan statements.'
|
|
67
67
|
)
|
|
68
68
|
setting 'openbolt_noop',
|
|
@@ -98,7 +98,7 @@ module ForemanOpenbolt
|
|
|
98
98
|
default: '',
|
|
99
99
|
full_name: N_('SSH Private Key'),
|
|
100
100
|
description: N_(
|
|
101
|
-
'Path on the smart proxy host to the private key used for SSH authentication. This key must be '
|
|
101
|
+
'Path on the smart proxy host to the private key used for SSH authentication. This key must be ' \
|
|
102
102
|
'readable by the foreman-proxy user.'
|
|
103
103
|
)
|
|
104
104
|
setting 'openbolt_run-as',
|
|
@@ -106,7 +106,7 @@ module ForemanOpenbolt
|
|
|
106
106
|
default: '',
|
|
107
107
|
full_name: N_('SSH Run As User'),
|
|
108
108
|
description: N_(
|
|
109
|
-
'The user to run commands as on the target host. This requires that the user specified '
|
|
109
|
+
'The user to run commands as on the target host. This requires that the user specified ' \
|
|
110
110
|
'in the "user" option has permission to run commands as this user.'
|
|
111
111
|
)
|
|
112
112
|
setting 'openbolt_sudo-password',
|
data/lib/proxy_api/openbolt.rb
CHANGED
|
@@ -6,11 +6,11 @@ module ProxyAPI
|
|
|
6
6
|
class Openbolt < Resource
|
|
7
7
|
def initialize(args)
|
|
8
8
|
@url = args[:url]
|
|
9
|
-
super
|
|
9
|
+
super
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def fetch_tasks
|
|
13
|
-
@tasks =
|
|
13
|
+
@tasks = parse_response(get('/openbolt/tasks'), 'fetch_tasks')
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def tasks
|
|
@@ -18,7 +18,7 @@ module ProxyAPI
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def reload_tasks
|
|
21
|
-
@tasks =
|
|
21
|
+
@tasks = parse_response(get('/openbolt/tasks/reload'), 'reload_tasks')
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def task_names
|
|
@@ -26,28 +26,44 @@ module ProxyAPI
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def openbolt_options
|
|
29
|
-
@openbolt_options ||=
|
|
29
|
+
@openbolt_options ||= parse_response(get('/openbolt/tasks/options'), 'openbolt_options')
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def launch_task(name:, targets:, parameters: {}, options: {})
|
|
33
|
-
|
|
33
|
+
response = post({
|
|
34
34
|
name: name,
|
|
35
35
|
targets: targets,
|
|
36
36
|
parameters: parameters,
|
|
37
37
|
options: options,
|
|
38
|
-
}.to_json, '/openbolt/launch/task')
|
|
38
|
+
}.to_json, '/openbolt/launch/task')
|
|
39
|
+
parse_response(response, 'launch_task')
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def job_status(job_id:)
|
|
42
|
-
|
|
43
|
+
parse_response(get("/openbolt/job/#{job_id}/status"), 'job_status')
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def job_result(job_id:)
|
|
46
|
-
|
|
47
|
+
parse_response(get("/openbolt/job/#{job_id}/result"), 'job_result')
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def delete_job_artifacts(job_id:)
|
|
50
|
-
|
|
51
|
+
parse_response(delete("/openbolt/job/#{job_id}/artifacts"), 'delete_job_artifacts')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_response(response, operation)
|
|
55
|
+
raise ProxyException.new(@url, nil, "No response from Smart Proxy during #{operation}") unless response
|
|
56
|
+
|
|
57
|
+
body = response.body
|
|
58
|
+
raise ProxyException.new(@url, nil, "Empty response body from Smart Proxy during #{operation}") if body.nil?
|
|
59
|
+
|
|
60
|
+
JSON.parse(body)
|
|
61
|
+
rescue JSON::ParserError => e
|
|
62
|
+
raise ProxyException.new(
|
|
63
|
+
@url, nil,
|
|
64
|
+
"Invalid JSON from Smart Proxy during #{operation}: #{e.message}. " \
|
|
65
|
+
"Response body (first 500 chars): #{body.to_s[0..500]}"
|
|
66
|
+
)
|
|
51
67
|
end
|
|
52
68
|
end
|
|
53
69
|
end
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'rake/testtask'
|
|
4
|
-
|
|
5
|
-
# Tasks
|
|
6
3
|
namespace :openbolt do
|
|
7
4
|
desc 'Refresh smart proxy features to detect OpenBolt feature'
|
|
8
5
|
task refresh_proxies: :environment do
|
|
@@ -20,7 +17,7 @@ namespace :openbolt do
|
|
|
20
17
|
end
|
|
21
18
|
end
|
|
22
19
|
|
|
23
|
-
if proxies.
|
|
20
|
+
if proxies.none?
|
|
24
21
|
puts "No smart proxies found"
|
|
25
22
|
else
|
|
26
23
|
openbolt_count = proxies.with_features('OpenBolt').count
|
|
@@ -28,21 +25,3 @@ namespace :openbolt do
|
|
|
28
25
|
end
|
|
29
26
|
end
|
|
30
27
|
end
|
|
31
|
-
|
|
32
|
-
# Tests
|
|
33
|
-
namespace :test do
|
|
34
|
-
desc 'Test ForemanOpenbolt'
|
|
35
|
-
Rake::TestTask.new(:foreman_openbolt) do |t|
|
|
36
|
-
test_dir = File.expand_path('../../test', __dir__)
|
|
37
|
-
t.libs << 'test'
|
|
38
|
-
t.libs << test_dir
|
|
39
|
-
t.pattern = "#{test_dir}/**/*_test.rb"
|
|
40
|
-
t.verbose = true
|
|
41
|
-
t.warning = false
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Rake::Task[:test].enhance ['test:foreman_openbolt']
|
|
46
|
-
|
|
47
|
-
load 'tasks/jenkins.rake'
|
|
48
|
-
Rake::Task['jenkins:unit'].enhance ['test:foreman_openbolt', 'foreman_openbolt:rubocop'] if Rake::Task.task_defined?(:'jenkins:unit')
|
data/locale/gemspec.rb
CHANGED
data/package.json
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foreman_openbolt",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "OpenBolt integration into Foreman",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "tfm-lint --plugin -d /webpack",
|
|
8
8
|
"test": "tfm-test --config jest.config.js",
|
|
9
9
|
"test:watch": "tfm-test --plugin --watchAll",
|
|
10
|
-
"test:current": "tfm-test --plugin --watch"
|
|
11
|
-
"publish-coverage": "tfm-publish-coverage",
|
|
12
|
-
"create-react-component": "yo react-domain"
|
|
10
|
+
"test:current": "tfm-test --plugin --watch"
|
|
13
11
|
},
|
|
14
12
|
"repository": {
|
|
15
13
|
"type": "git",
|
|
@@ -19,23 +17,21 @@
|
|
|
19
17
|
"url": "http://projects.theforeman.org/projects/foreman_openbolt/issues"
|
|
20
18
|
},
|
|
21
19
|
"peerDependencies": {
|
|
22
|
-
"@theforeman/vendor": ">=
|
|
20
|
+
"@theforeman/vendor": ">= 15.0.1"
|
|
23
21
|
},
|
|
24
22
|
"dependencies": {
|
|
25
23
|
"react-intl": "^2.8.0"
|
|
26
24
|
},
|
|
27
25
|
"devDependencies": {
|
|
28
26
|
"@babel/core": "^7.7.0",
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@theforeman/
|
|
32
|
-
"@theforeman/
|
|
33
|
-
"@theforeman/test": "
|
|
34
|
-
"@theforeman/vendor-dev": "
|
|
27
|
+
"@testing-library/react": "^10.4.9",
|
|
28
|
+
"@testing-library/react-hooks": "^3.7.0",
|
|
29
|
+
"@theforeman/builder": ">= 15.0.1",
|
|
30
|
+
"@theforeman/eslint-plugin-foreman": ">= 15.0.1",
|
|
31
|
+
"@theforeman/test": ">= 15.0.1",
|
|
32
|
+
"@theforeman/vendor-dev": ">= 15.0.1",
|
|
35
33
|
"babel-eslint": "^10.0.3",
|
|
36
|
-
"eslint": "^6.
|
|
37
|
-
"prettier": "^1.19.1"
|
|
38
|
-
"stylelint-config-standard": "^18.0.0",
|
|
39
|
-
"stylelint": "^9.3.0"
|
|
34
|
+
"eslint": "^6.8.0",
|
|
35
|
+
"prettier": "^1.19.1"
|
|
40
36
|
}
|
|
41
37
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'capybara'
|
|
4
|
+
require 'capybara/dsl'
|
|
5
|
+
require 'selenium-webdriver'
|
|
6
|
+
require 'test/unit'
|
|
7
|
+
|
|
8
|
+
# Base class for acceptance tests using Capybara + Selenium Chrome.
|
|
9
|
+
# Connects to Foreman through a remote ChromeDriver container and
|
|
10
|
+
# exercises the plugin UI as a real user would.
|
|
11
|
+
class AcceptanceTestCase < Test::Unit::TestCase
|
|
12
|
+
include Capybara::DSL
|
|
13
|
+
|
|
14
|
+
# Chrome runs in a separate container and reaches Foreman via the Docker
|
|
15
|
+
# network service name. The test runner connects to ChromeDriver via the
|
|
16
|
+
# exposed port 4444 on the host.
|
|
17
|
+
FOREMAN_URL = ENV.fetch('FOREMAN_URL', 'https://foreman')
|
|
18
|
+
FOREMAN_USER = ENV.fetch('FOREMAN_USER', 'admin')
|
|
19
|
+
FOREMAN_PASS = ENV.fetch('FOREMAN_PASS', 'changeme')
|
|
20
|
+
CHROMEDRIVER_URL = ENV.fetch('CHROMEDRIVER_URL', 'http://localhost:4444')
|
|
21
|
+
|
|
22
|
+
def setup
|
|
23
|
+
Capybara.app_host = FOREMAN_URL
|
|
24
|
+
Capybara.run_server = false
|
|
25
|
+
Capybara.default_max_wait_time = 15
|
|
26
|
+
|
|
27
|
+
Capybara.register_driver :remote_chrome do |app|
|
|
28
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
|
29
|
+
options.add_argument('--headless') unless ENV['HEADFUL']
|
|
30
|
+
options.add_argument('--no-sandbox')
|
|
31
|
+
options.add_argument('--disable-dev-shm-usage')
|
|
32
|
+
options.add_argument('--disable-gpu')
|
|
33
|
+
options.add_argument('--window-size=1280,720')
|
|
34
|
+
options.add_argument('--ignore-certificate-errors')
|
|
35
|
+
|
|
36
|
+
Capybara::Selenium::Driver.new(
|
|
37
|
+
app,
|
|
38
|
+
browser: :remote,
|
|
39
|
+
url: CHROMEDRIVER_URL,
|
|
40
|
+
options: options
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Capybara.default_driver = :remote_chrome
|
|
45
|
+
Capybara.javascript_driver = :remote_chrome
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def teardown
|
|
49
|
+
visit '/users/logout'
|
|
50
|
+
Capybara.reset_sessions!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def foreman_login(user: FOREMAN_USER, password: FOREMAN_PASS)
|
|
54
|
+
visit '/users/login'
|
|
55
|
+
fill_in 'login_login', with: user
|
|
56
|
+
fill_in 'login_password', with: password
|
|
57
|
+
click_button 'Log In'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# --- Launch page helpers ---
|
|
61
|
+
|
|
62
|
+
def select_first_proxy
|
|
63
|
+
assert_selector '#smart-proxy-input option', minimum: 2, wait: 15
|
|
64
|
+
proxy_option = first('#smart-proxy-input option:not([value=""])')
|
|
65
|
+
select proxy_option.text, from: 'smart-proxy-input'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def select_hosts_via_search(query)
|
|
69
|
+
find('[aria-label="Select host targeting method"]').click
|
|
70
|
+
find('[data-ouia-component-id="host_methods"]').find('li', text: 'Search query').click
|
|
71
|
+
search_input = find('.foreman-search-field input[type="text"]', wait: 10)
|
|
72
|
+
search_input.fill_in with: query
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def launch_task_via_ui(task_name, targets: 'target', params: {})
|
|
76
|
+
visit '/foreman_openbolt/page_launch_task'
|
|
77
|
+
assert_selector '#smart-proxy-input', wait: 15
|
|
78
|
+
|
|
79
|
+
select_first_proxy
|
|
80
|
+
select_hosts_via_search(targets)
|
|
81
|
+
|
|
82
|
+
assert_selector '#task-name-input option', minimum: 2, wait: 15
|
|
83
|
+
select task_name, from: 'task-name-input'
|
|
84
|
+
|
|
85
|
+
params.each do |name, value|
|
|
86
|
+
assert_selector "#param_#{name}", wait: 10
|
|
87
|
+
fill_in "param_#{name}", with: value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
click_button 'Launch Task'
|
|
91
|
+
assert_selector 'h1', text: 'Task Execution', wait: 15
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assert_task_completed
|
|
95
|
+
assert_selector '.pf-v5-c-label', text: /Success|Complete/i, wait: 120
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def assert_task_failed
|
|
99
|
+
assert_selector '.pf-v5-c-label', text: /Failed|Failure|Error/i, wait: 120
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def assert_result_contains(text)
|
|
103
|
+
assert_selector '.pf-v5-c-code-block__code', text: text, wait: 15
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def assert_result_has_content
|
|
107
|
+
assert_no_selector '.pf-v5-c-empty-state', text: 'No result data', wait: 15
|
|
108
|
+
assert_selector '.pf-v5-c-code-block__code', wait: 15
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def assert_log_contains(text)
|
|
112
|
+
# Click the "Log Output" tab to see the bolt command and log
|
|
113
|
+
find('.pf-v5-c-tabs__link', text: 'Log Output').click
|
|
114
|
+
assert_selector '.pf-v5-c-code-block__code', text: text, wait: 15
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# OpenBolt options and task parameters both use param_ prefix for field IDs
|
|
118
|
+
def set_openbolt_option(name, value)
|
|
119
|
+
field = find("#param_#{name}", wait: 10)
|
|
120
|
+
if field.tag_name == 'select'
|
|
121
|
+
field.select value
|
|
122
|
+
elsif field['type'] == 'checkbox'
|
|
123
|
+
value ? field.check : field.uncheck
|
|
124
|
+
else
|
|
125
|
+
field.fill_in with: value
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# --- Settings page helpers ---
|
|
130
|
+
|
|
131
|
+
# Update a Foreman setting through the /settings UI. Each setting row
|
|
132
|
+
# has an inline edit button with id=<setting_name> that reveals an
|
|
133
|
+
# input with id=setting-input-<setting_name> and a submit button with
|
|
134
|
+
# ouiaId=submit-edit-btn (see Foreman's SettingValueCell /
|
|
135
|
+
# SettingValueEdit components).
|
|
136
|
+
def update_foreman_setting(setting_name, new_value, category: 'openbolt')
|
|
137
|
+
visit '/settings'
|
|
138
|
+
find("a[href='##{category}_settings_tab']", wait: 10).click
|
|
139
|
+
|
|
140
|
+
within("##{category}_settings_tab", wait: 10) do
|
|
141
|
+
find("button##{setting_name}", wait: 10).click
|
|
142
|
+
find("#setting-input-#{setting_name}", wait: 10).fill_in with: new_value
|
|
143
|
+
find("[data-ouia-component-id='submit-edit-btn']").click
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|