foreman_remote_execution 3.3.1 → 3.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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +22 -1
- data/app/controllers/api/v2/template_invocations_controller.rb +4 -1
- data/app/helpers/job_invocations_helper.rb +1 -1
- data/app/helpers/remote_execution_helper.rb +38 -33
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +1 -1
- data/app/views/api/v2/job_invocations/main.json.rabl +8 -2
- data/app/views/job_invocations/_tab_hosts.html.erb +3 -23
- data/app/views/job_invocations/show.html.erb +0 -6
- data/app/views/job_invocations/show.json.erb +4 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +42 -14
- data/test/unit/concerns/host_extensions_test.rb +7 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +1 -0
- data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +3 -0
- data/webpack/__mocks__/foremanReact/constants.js +3 -0
- data/webpack/index.js +8 -5
- data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +52 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +8 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +1 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +12 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +6 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +6 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +6 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +31 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +12 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +81 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +43 -0
- data/webpack/react_app/components/TargetingHosts/components/HostItem.js +39 -0
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +54 -0
- data/webpack/react_app/components/TargetingHosts/index.js +37 -0
- metadata +24 -10
- data/app/views/job_invocations/_host_actions_td.html.erb +0 -3
- data/app/views/job_invocations/_host_name_td.html.erb +0 -8
- data/app/views/job_invocations/_host_status_td.html.erb +0 -1
- data/app/views/job_invocations/show.js.erb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d093d837d0009e477549fba4700f3e0090045ba23f8b2c50fb05439fee53e403
|
4
|
+
data.tar.gz: 54dffc56caa3fa1bb72bb159b152d267d2e348b64b57322ea74cdf0ad9289cc1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3524503031c73f86a0f3ed0b6c8dd39499f85df47052eca941f455552665b93ff041e5d6df9870cfb95ac1219017a561a483556306905bdc005caf252a5e088
|
7
|
+
data.tar.gz: 537da427406ce7c794d5e589e7e82f8c6fbda33a3a4231129905b69171421f41e98563c0496d9c7cc748f675d200729ec52f4d9067a8f7879972922358a2a868
|
data/Gemfile
CHANGED
@@ -18,7 +18,17 @@ module Api
|
|
18
18
|
|
19
19
|
api :GET, '/job_invocations/:id', N_('Show job invocation')
|
20
20
|
param :id, :identifier, :required => true
|
21
|
+
param :host_status, String, required: false, allow_blank: true, desc: N_('Show Job status for the hosts.')
|
21
22
|
def show
|
23
|
+
@hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
24
|
+
@template_invocations = @job_invocation.template_invocations
|
25
|
+
.where(host: @hosts)
|
26
|
+
.includes(:input_values)
|
27
|
+
|
28
|
+
if params[:host_status]
|
29
|
+
template_invocations = @template_invocations.includes(:run_host_job_task).to_a
|
30
|
+
@host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
|
31
|
+
end
|
22
32
|
end
|
23
33
|
|
24
34
|
def_param_group :job_invocation do
|
@@ -146,7 +156,7 @@ module Api
|
|
146
156
|
end
|
147
157
|
|
148
158
|
def find_host
|
149
|
-
@host =
|
159
|
+
@host = @nested_obj.targeting.hosts.authorized(:view_hosts, Host).find(params['host_id'])
|
150
160
|
rescue ActiveRecord::RecordNotFound
|
151
161
|
not_found({ :error => { :message => (_("Host with id '%{id}' was not found") % { :id => params['host_id'] }) } })
|
152
162
|
end
|
@@ -204,6 +214,17 @@ module Api
|
|
204
214
|
def parent_scope
|
205
215
|
resource_class.where(nil)
|
206
216
|
end
|
217
|
+
|
218
|
+
def template_invocation_status(template_invocation)
|
219
|
+
task = template_invocation.try(:run_host_job_task)
|
220
|
+
parent_task = @job_invocation.task
|
221
|
+
|
222
|
+
return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
|
223
|
+
return task.state if task.state == 'running' || task.state == 'planned'
|
224
|
+
return 'error' if task.result == 'warning'
|
225
|
+
|
226
|
+
task.result
|
227
|
+
end
|
207
228
|
end
|
208
229
|
end
|
209
230
|
end
|
@@ -26,7 +26,10 @@ module Api
|
|
26
26
|
private
|
27
27
|
|
28
28
|
def resource_scope_for_template_invocations
|
29
|
-
@job_invocation.template_invocations
|
29
|
+
@job_invocation.template_invocations
|
30
|
+
.includes(:host)
|
31
|
+
.where(host: Host.authorized(:view_hosts, Host))
|
32
|
+
.search_for(*search_options)
|
30
33
|
end
|
31
34
|
|
32
35
|
def find_job_invocation
|
@@ -29,7 +29,7 @@ module JobInvocationsHelper
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def preview_hosts(template_invocation)
|
32
|
-
hosts = template_invocation.targeting.hosts.take(20)
|
32
|
+
hosts = template_invocation.targeting.hosts.authorized(:view_hosts, Host).take(20)
|
33
33
|
hosts.map do |host|
|
34
34
|
collapsed_preview(host) +
|
35
35
|
render(:partial => 'job_invocations/user_input',
|
@@ -14,43 +14,35 @@ module RemoteExecutionHelper
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def template_invocation_status(task, parent_task)
|
17
|
-
if task.nil?
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
elsif task.state == 'running'
|
24
|
-
icon_text('running', _('running'), :kind => 'pficon')
|
25
|
-
elsif task.state == 'planned'
|
26
|
-
icon_text('build', _('planned'), :kind => 'pficon')
|
27
|
-
else
|
28
|
-
case task.result
|
29
|
-
when 'warning', 'error'
|
30
|
-
icon_text('error-circle-o', _('failed'), :kind => 'pficon')
|
31
|
-
when 'cancelled'
|
32
|
-
icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
|
33
|
-
when 'success'
|
34
|
-
icon_text('ok', _('success'), :kind => 'pficon')
|
35
|
-
else
|
36
|
-
task.result
|
37
|
-
end
|
38
|
-
end
|
17
|
+
return(parent_task.result == 'cancelled' ? _('cancelled') : 'N/A') if task.nil?
|
18
|
+
return task.state if task.state == 'running' || task.state == 'planned'
|
19
|
+
return _('error') if task.result == 'warning'
|
20
|
+
|
21
|
+
task.result
|
39
22
|
end
|
40
23
|
|
41
24
|
def template_invocation_actions(task, host, job_invocation, template_invocation)
|
25
|
+
links = []
|
42
26
|
host_task = template_invocation.try(:run_host_job_task)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
27
|
+
|
28
|
+
if authorized_for(hash_for_host_path(host).merge(auth_object: host, permission: :view_hosts, authorizer: job_hosts_authorizer))
|
29
|
+
links << { title: _('Host detail'),
|
30
|
+
action: { href: host_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
|
31
|
+
end
|
32
|
+
|
33
|
+
if authorized_for(hash_for_rerun_job_invocation_path(id: job_invocation, host_ids: [ host.id ], authorizer: job_hosts_authorizer))
|
34
|
+
links << { title: (_('Rerun on %s') % host.name),
|
35
|
+
action: { href: rerun_job_invocation_path(job_invocation, host_ids: [ host.id ]),
|
36
|
+
'data-method': 'get', id: "#{host.name}-actions-rerun" } }
|
37
|
+
end
|
38
|
+
|
39
|
+
if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
|
40
|
+
links << { title: _('Host task'),
|
41
|
+
action: { href: foreman_tasks_task_path(host_task),
|
42
|
+
'data-method': 'get', id: "#{host.name}-actions-task" } }
|
43
|
+
end
|
44
|
+
|
45
|
+
links
|
54
46
|
end
|
55
47
|
|
56
48
|
def remote_execution_provider_for(template_invocation)
|
@@ -237,4 +229,17 @@ module RemoteExecutionHelper
|
|
237
229
|
|
238
230
|
task.execution_plan.actions[1].try(:input).try(:[], 'script')
|
239
231
|
end
|
232
|
+
|
233
|
+
def targeting_hosts(job_invocation, hosts)
|
234
|
+
hosts.map do |host|
|
235
|
+
template_invocation = job_invocation.template_invocations.find { |template_inv| template_inv.host_id == host.id }
|
236
|
+
task = template_invocation.try(:run_host_job_task)
|
237
|
+
link_authorized = !task.nil? && authorized_for(hash_for_template_invocation_path(:id => template_invocation).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer))
|
238
|
+
|
239
|
+
{ name: host.name,
|
240
|
+
link: link_authorized ? template_invocation_path(:id => template_invocation) : '',
|
241
|
+
status: template_invocation_status(task, job_invocation.task),
|
242
|
+
actions: template_invocation_actions(task, host, job_invocation, template_invocation) }
|
243
|
+
end
|
244
|
+
end
|
240
245
|
end
|
@@ -49,7 +49,7 @@ module ForemanRemoteExecution
|
|
49
49
|
keys = remote_execution_ssh_keys
|
50
50
|
source = 'global'
|
51
51
|
if keys.present?
|
52
|
-
value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| v
|
52
|
+
value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
|
53
53
|
params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source}
|
54
54
|
end
|
55
55
|
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
@@ -19,8 +19,14 @@ child :targeting do
|
|
19
19
|
attributes :bookmark_id, :search_query, :targeting_type, :user_id, :status, :status_label,
|
20
20
|
:randomized_ordering
|
21
21
|
|
22
|
-
child
|
22
|
+
child @hosts do
|
23
23
|
extends 'api/v2/hosts/base'
|
24
|
+
|
25
|
+
if params[:host_status]
|
26
|
+
node :job_status do |host|
|
27
|
+
@host_statuses[host.id]
|
28
|
+
end
|
29
|
+
end
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
@@ -28,7 +34,7 @@ child :task do
|
|
28
34
|
attributes :id, :state
|
29
35
|
end
|
30
36
|
|
31
|
-
child
|
37
|
+
child @template_invocations do
|
32
38
|
attributes :template_id, :template_name
|
33
39
|
child :input_values do
|
34
40
|
attributes :template_input_name, :template_input_id
|
@@ -15,29 +15,9 @@
|
|
15
15
|
<% end %>
|
16
16
|
<br>
|
17
17
|
|
18
|
-
<
|
19
|
-
|
20
|
-
|
21
|
-
<th><%= sort :name, :as => _('Host') %></th>
|
22
|
-
<th><%= _('Status') %></th>
|
23
|
-
<th><%= _('Actions') %></th>
|
24
|
-
</tr>
|
25
|
-
</thead>
|
26
|
-
|
27
|
-
<tbody>
|
28
|
-
<% hosts.each do |host| %>
|
29
|
-
<% template_invocation = job_invocation.template_invocations.find { |template_invocation| template_invocation.host_id == host.id } %>
|
30
|
-
<% task = template_invocation.try(:run_host_job_task) %>
|
31
|
-
<tr>
|
32
|
-
<% options = { :host => host, :task => task, :job_invocation => job_invocation, :template_invocation => template_invocation } %>
|
33
|
-
<%= render 'host_name_td', options %>
|
34
|
-
<%= render 'host_status_td', options %>
|
35
|
-
<%= render 'host_actions_td', options %>
|
36
|
-
</tr>
|
37
|
-
<% end %>
|
38
|
-
</tbody>
|
39
|
-
</table>
|
40
|
-
|
18
|
+
<div id="targeting_hosts">
|
19
|
+
<%= mount_react_component('TargetingHosts', '#targeting_hosts') %>
|
20
|
+
</div>
|
41
21
|
<%= will_paginate_with_info @hosts, :container => true %>
|
42
22
|
<% else %>
|
43
23
|
<div class="alert alert-warning">
|
@@ -41,9 +41,3 @@
|
|
41
41
|
<% end %>
|
42
42
|
<%= render_tab_content_for(:main_tabs, subject: @job_invocation) %>
|
43
43
|
</div>
|
44
|
-
|
45
|
-
<script id="job_invocation_refresh" data-refresh-url="<%= job_invocation_path(@job_invocation) %>">
|
46
|
-
<% if @auto_refresh %>
|
47
|
-
delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), job_invocation_refresh_data());
|
48
|
-
<% end %>
|
49
|
-
</script>
|
@@ -29,6 +29,6 @@ Gem::Specification.new do |s|
|
|
29
29
|
s.add_dependency 'foreman-tasks', '>= 0.15.1'
|
30
30
|
|
31
31
|
s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
|
32
|
-
s.add_development_dependency 'rubocop'
|
32
|
+
s.add_development_dependency 'rubocop', '~> 0.80.0'
|
33
33
|
s.add_development_dependency 'rdoc'
|
34
34
|
end
|
@@ -4,7 +4,7 @@ module Api
|
|
4
4
|
module V2
|
5
5
|
class JobInvocationsControllerTest < ActionController::TestCase
|
6
6
|
setup do
|
7
|
-
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
7
|
+
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
|
8
8
|
@template = FactoryBot.create(:job_template, :with_input)
|
9
9
|
|
10
10
|
# Without this the template in template_invocations and in pattern_template_invocations
|
@@ -20,18 +20,32 @@ module Api
|
|
20
20
|
assert_response :success
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
describe 'show' do
|
24
|
+
test 'should get invocation detail' do
|
25
|
+
get :show, params: { :id => @invocation.id }
|
26
|
+
assert_response :success
|
27
|
+
template = ActiveSupport::JSON.decode(@response.body)
|
28
|
+
assert_not_empty template
|
29
|
+
assert_equal template['job_category'], @invocation.job_category
|
30
|
+
assert_not_empty template['targeting']['hosts']
|
31
|
+
end
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
test 'should get invocation detail when taxonomies are set' do
|
34
|
+
taxonomy_params = %w(organization location).reduce({}) { |acc, cur| acc.merge("#{cur}_id" => FactoryBot.create(cur)) }
|
35
|
+
get :show, params: taxonomy_params.merge(:id => @invocation.id)
|
36
|
+
assert_response :success
|
37
|
+
end
|
38
|
+
|
39
|
+
test 'should see only permitted hosts' do
|
40
|
+
@user = FactoryBot.create(:user, admin: false)
|
41
|
+
setup_user('view', 'job_invocations', nil, @user)
|
42
|
+
setup_user('view', 'hosts', 'name ~ nope.example.com', @user)
|
43
|
+
|
44
|
+
get :show, params: { :id => @invocation.id }, session: set_session_user(@user)
|
45
|
+
assert_response :success
|
46
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
47
|
+
assert_empty response['targeting']['hosts']
|
48
|
+
end
|
35
49
|
end
|
36
50
|
|
37
51
|
context 'creation' do
|
@@ -108,7 +122,7 @@ module Api
|
|
108
122
|
end
|
109
123
|
|
110
124
|
describe '#output' do
|
111
|
-
let(:host) { @invocation.
|
125
|
+
let(:host) { @invocation.targeting.hosts.first }
|
112
126
|
|
113
127
|
test 'should provide output for delayed task' do
|
114
128
|
ForemanTasks::Task.any_instance.expects(:scheduled?).returns(true)
|
@@ -137,6 +151,12 @@ module Api
|
|
137
151
|
assert_equal result['message'], "Job invocation not found by id '#{invocation_id}'"
|
138
152
|
assert_response :missing
|
139
153
|
end
|
154
|
+
|
155
|
+
test 'should get output only for host in job invocation' do
|
156
|
+
get :output, params: { job_invocation_id: @invocation.id,
|
157
|
+
host_id: FactoryBot.create(:host).id }
|
158
|
+
assert_response :missing
|
159
|
+
end
|
140
160
|
end
|
141
161
|
|
142
162
|
describe 'raw output' do
|
@@ -148,7 +168,7 @@ module Api
|
|
148
168
|
let(:fake_task) do
|
149
169
|
OpenStruct.new :pending? => false, :main_action => OpenStruct.new(:live_output => fake_output)
|
150
170
|
end
|
151
|
-
let(:host) { @invocation.
|
171
|
+
let(:host) { @invocation.targeting.hosts.first }
|
152
172
|
|
153
173
|
test 'should provide raw output for a host' do
|
154
174
|
JobInvocation.any_instance.expects(:task).returns(OpenStruct.new(:scheduled? => false))
|
@@ -184,6 +204,12 @@ module Api
|
|
184
204
|
assert_nil result['output']
|
185
205
|
assert_response :success
|
186
206
|
end
|
207
|
+
|
208
|
+
test 'should get raw output only for host in job invocation' do
|
209
|
+
get :raw_output, params: { job_invocation_id: @invocation.id,
|
210
|
+
host_id: FactoryBot.create(:host).id }
|
211
|
+
assert_response :missing
|
212
|
+
end
|
187
213
|
end
|
188
214
|
|
189
215
|
test 'should cancel a job' do
|
@@ -232,11 +258,13 @@ module Api
|
|
232
258
|
end
|
233
259
|
|
234
260
|
test 'should not raise an exception when reruning failed has no hosts' do
|
261
|
+
@invocation.targeting.hosts.first.destroy
|
235
262
|
JobInvocation.any_instance.expects(:generate_description)
|
236
263
|
JobInvocationComposer.any_instance
|
237
264
|
.expects(:validate_job_category)
|
238
265
|
.with(@invocation.job_category)
|
239
266
|
.returns(@invocation.job_category)
|
267
|
+
|
240
268
|
post :rerun, params: { :id => @invocation.id, :failed_only => true }
|
241
269
|
assert_response :success
|
242
270
|
result = ActiveSupport::JSON.decode(@response.body)
|
@@ -48,6 +48,13 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
48
48
|
_(host.host_param('remote_execution_ssh_keys')).must_include sshkey
|
49
49
|
end
|
50
50
|
|
51
|
+
it 'merges ssh key as a string from host parameters and proxies' do
|
52
|
+
key = 'ssh-rsa not-even-a-key something@somewhere.com'
|
53
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_keys', :value => key)
|
54
|
+
_(host.host_param('remote_execution_ssh_keys')).must_include key
|
55
|
+
_(host.host_param('remote_execution_ssh_keys')).must_include sshkey
|
56
|
+
end
|
57
|
+
|
51
58
|
it 'has ssh keys in the parameters even when no user specified' do
|
52
59
|
# this is a case, when using the helper in provisioning templates
|
53
60
|
FactoryBot.create(:smart_proxy, :ssh)
|
@@ -0,0 +1 @@
|
|
1
|
+
export const translate = s => s;
|
data/webpack/index.js
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
2
1
|
import { registerReducer } from 'foremanReact/common/MountingService';
|
3
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
4
2
|
import componentRegistry from 'foremanReact/components/componentRegistry';
|
5
3
|
import JobInvocationContainer from './react_app/components/jobInvocations';
|
4
|
+
import TargetingHosts from './react_app/components/TargetingHosts';
|
6
5
|
import rootReducer from './react_app/redux/reducers';
|
7
6
|
|
8
|
-
|
9
|
-
name: 'JobInvocationContainer',
|
10
|
-
type:
|
7
|
+
const components = [
|
8
|
+
{ name: 'JobInvocationContainer', type: JobInvocationContainer },
|
9
|
+
{ name: 'TargetingHosts', type: TargetingHosts },
|
10
|
+
];
|
11
|
+
|
12
|
+
components.forEach(component => {
|
13
|
+
componentRegistry.register(component);
|
11
14
|
});
|
12
15
|
|
13
16
|
registerReducer('foremanRemoteExecutionReducers', rootReducer);
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { LoadingState, Alert } from 'patternfly-react';
|
5
|
+
import { STATUS } from 'foremanReact/constants';
|
6
|
+
import HostItem from './components/HostItem';
|
7
|
+
|
8
|
+
const TargetingHosts = ({ status, items }) => {
|
9
|
+
if (status === STATUS.ERROR) {
|
10
|
+
return (
|
11
|
+
<Alert type="error">
|
12
|
+
{__(
|
13
|
+
'There was an error while updating the status, try refreshing the page.'
|
14
|
+
)}
|
15
|
+
</Alert>
|
16
|
+
);
|
17
|
+
}
|
18
|
+
|
19
|
+
return (
|
20
|
+
<LoadingState loading={!items.length}>
|
21
|
+
<div>
|
22
|
+
<table className="table table-bordered table-striped table-hover">
|
23
|
+
<thead>
|
24
|
+
<tr>
|
25
|
+
<th>{__('Host')}</th>
|
26
|
+
<th>{__('Status')}</th>
|
27
|
+
<th>{__('Actions')}</th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tbody>
|
31
|
+
{items.map(host => (
|
32
|
+
<HostItem
|
33
|
+
key={host.name}
|
34
|
+
name={host.name}
|
35
|
+
link={host.link}
|
36
|
+
status={host.status}
|
37
|
+
actions={host.actions}
|
38
|
+
/>
|
39
|
+
))}
|
40
|
+
</tbody>
|
41
|
+
</table>
|
42
|
+
</div>
|
43
|
+
</LoadingState>
|
44
|
+
);
|
45
|
+
};
|
46
|
+
|
47
|
+
TargetingHosts.propTypes = {
|
48
|
+
status: PropTypes.string.isRequired,
|
49
|
+
items: PropTypes.array.isRequired,
|
50
|
+
};
|
51
|
+
|
52
|
+
export default TargetingHosts;
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { getURI } from 'foremanReact/common/urlHelpers';
|
2
|
+
import { get } from 'foremanReact/redux/API';
|
3
|
+
import { withInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
4
|
+
import { TARGETING_HOSTS } from './TargetingHostsConsts';
|
5
|
+
|
6
|
+
const url = getURI().addQuery('format', 'json');
|
7
|
+
export const getData = () =>
|
8
|
+
withInterval(get({ key: TARGETING_HOSTS, url }), 1000);
|
@@ -0,0 +1 @@
|
|
1
|
+
export const TARGETING_HOSTS = 'TARGETING_HOSTS';
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import {
|
2
|
+
selectAPIStatus,
|
3
|
+
selectAPIResponse,
|
4
|
+
} from 'foremanReact/redux/API/APISelectors';
|
5
|
+
import { TARGETING_HOSTS } from './TargetingHostsConsts';
|
6
|
+
|
7
|
+
export const selectItems = state =>
|
8
|
+
selectAPIResponse(state, TARGETING_HOSTS).hosts || [];
|
9
|
+
|
10
|
+
export const selectAutoRefresh = state =>
|
11
|
+
selectAPIResponse(state, TARGETING_HOSTS).autoRefresh;
|
12
|
+
export const selectStatus = state => selectAPIStatus(state, TARGETING_HOSTS);
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
|
2
|
+
import HostStatus from '../components/HostStatus';
|
3
|
+
import { HostStatusFixtures } from './fixtures';
|
4
|
+
|
5
|
+
describe('HostStatus', () =>
|
6
|
+
testComponentSnapshotsWithFixtures(HostStatus, HostStatusFixtures));
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
|
2
|
+
import TargetingHosts from '../TargetingHosts';
|
3
|
+
import { TargetingHostsFixtures } from './fixtures';
|
4
|
+
|
5
|
+
describe('TargetingHosts', () =>
|
6
|
+
testComponentSnapshotsWithFixtures(TargetingHosts, TargetingHostsFixtures));
|
data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
2
|
+
|
3
|
+
exports[`HostItem renders 1`] = `
|
4
|
+
<tr
|
5
|
+
id="targeting-host-Host1"
|
6
|
+
>
|
7
|
+
<td
|
8
|
+
className="host_name"
|
9
|
+
>
|
10
|
+
<a
|
11
|
+
href="/host1"
|
12
|
+
>
|
13
|
+
Host1
|
14
|
+
</a>
|
15
|
+
</td>
|
16
|
+
<td
|
17
|
+
className="host_status"
|
18
|
+
>
|
19
|
+
<HostStatus
|
20
|
+
status="success"
|
21
|
+
/>
|
22
|
+
</td>
|
23
|
+
<td
|
24
|
+
className="host_actions"
|
25
|
+
>
|
26
|
+
<ActionButtons
|
27
|
+
buttons={Array []}
|
28
|
+
/>
|
29
|
+
</td>
|
30
|
+
</tr>
|
31
|
+
`;
|
data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
2
|
+
|
3
|
+
exports[`TargetingHosts renders 1`] = `
|
4
|
+
<LoadingState
|
5
|
+
additionalClasses=""
|
6
|
+
loading={false}
|
7
|
+
loadingText="Loading"
|
8
|
+
size="lg"
|
9
|
+
timeout={300}
|
10
|
+
>
|
11
|
+
<div>
|
12
|
+
<table
|
13
|
+
className="table table-bordered table-striped table-hover"
|
14
|
+
>
|
15
|
+
<thead>
|
16
|
+
<tr>
|
17
|
+
<th>
|
18
|
+
Host
|
19
|
+
</th>
|
20
|
+
<th>
|
21
|
+
Status
|
22
|
+
</th>
|
23
|
+
<th>
|
24
|
+
Actions
|
25
|
+
</th>
|
26
|
+
</tr>
|
27
|
+
</thead>
|
28
|
+
<tbody>
|
29
|
+
<HostItem
|
30
|
+
actions={Array []}
|
31
|
+
key="host"
|
32
|
+
link="/link"
|
33
|
+
name="host"
|
34
|
+
status="success"
|
35
|
+
/>
|
36
|
+
</tbody>
|
37
|
+
</table>
|
38
|
+
</div>
|
39
|
+
</LoadingState>
|
40
|
+
`;
|
41
|
+
|
42
|
+
exports[`TargetingHosts renders with error 1`] = `
|
43
|
+
<Alert
|
44
|
+
className=""
|
45
|
+
onDismiss={null}
|
46
|
+
type="error"
|
47
|
+
>
|
48
|
+
There was an error while updating the status, try refreshing the page.
|
49
|
+
</Alert>
|
50
|
+
`;
|
51
|
+
|
52
|
+
exports[`TargetingHosts renders with loading 1`] = `
|
53
|
+
<LoadingState
|
54
|
+
additionalClasses=""
|
55
|
+
loading={true}
|
56
|
+
loadingText="Loading"
|
57
|
+
size="lg"
|
58
|
+
timeout={300}
|
59
|
+
>
|
60
|
+
<div>
|
61
|
+
<table
|
62
|
+
className="table table-bordered table-striped table-hover"
|
63
|
+
>
|
64
|
+
<thead>
|
65
|
+
<tr>
|
66
|
+
<th>
|
67
|
+
Host
|
68
|
+
</th>
|
69
|
+
<th>
|
70
|
+
Status
|
71
|
+
</th>
|
72
|
+
<th>
|
73
|
+
Actions
|
74
|
+
</th>
|
75
|
+
</tr>
|
76
|
+
</thead>
|
77
|
+
<tbody />
|
78
|
+
</table>
|
79
|
+
</div>
|
80
|
+
</LoadingState>
|
81
|
+
`;
|
@@ -0,0 +1,43 @@
|
|
1
|
+
export const HostItemFixtures = {
|
2
|
+
renders: {
|
3
|
+
name: 'Host1',
|
4
|
+
link: '/host1',
|
5
|
+
status: 'success',
|
6
|
+
actions: [],
|
7
|
+
},
|
8
|
+
};
|
9
|
+
|
10
|
+
export const HostStatusFixtures = {
|
11
|
+
renders: {
|
12
|
+
status: 'success',
|
13
|
+
},
|
14
|
+
};
|
15
|
+
|
16
|
+
export const TargetingHostsFixtures = {
|
17
|
+
renders: {
|
18
|
+
status: '',
|
19
|
+
items: [
|
20
|
+
{
|
21
|
+
name: 'host',
|
22
|
+
link: '/link',
|
23
|
+
status: 'success',
|
24
|
+
actions: [],
|
25
|
+
},
|
26
|
+
],
|
27
|
+
},
|
28
|
+
'renders with error': {
|
29
|
+
status: 'ERROR',
|
30
|
+
items: [
|
31
|
+
{
|
32
|
+
name: 'host',
|
33
|
+
link: '/link',
|
34
|
+
status: 'success',
|
35
|
+
actions: [],
|
36
|
+
},
|
37
|
+
],
|
38
|
+
},
|
39
|
+
'renders with loading': {
|
40
|
+
status: '',
|
41
|
+
items: [],
|
42
|
+
},
|
43
|
+
};
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { ActionButtons } from 'foremanReact/components/common/ActionButtons/ActionButtons';
|
4
|
+
import HostStatus from './HostStatus';
|
5
|
+
|
6
|
+
const HostItem = ({ name, link, status, actions }) => {
|
7
|
+
const hostLink = link ? (
|
8
|
+
<a href={link}>{name}</a>
|
9
|
+
) : (
|
10
|
+
<a href="#" className="disabled">
|
11
|
+
{name}
|
12
|
+
</a>
|
13
|
+
);
|
14
|
+
|
15
|
+
return (
|
16
|
+
<tr id={`targeting-host-${name}`}>
|
17
|
+
<td className="host_name">{hostLink}</td>
|
18
|
+
<td className="host_status">
|
19
|
+
<HostStatus status={status} />
|
20
|
+
</td>
|
21
|
+
<td className="host_actions">
|
22
|
+
<ActionButtons buttons={[...actions]} />
|
23
|
+
</td>
|
24
|
+
</tr>
|
25
|
+
);
|
26
|
+
};
|
27
|
+
|
28
|
+
HostItem.propTypes = {
|
29
|
+
name: PropTypes.string.isRequired,
|
30
|
+
link: PropTypes.string.isRequired,
|
31
|
+
status: PropTypes.string.isRequired,
|
32
|
+
actions: PropTypes.array,
|
33
|
+
};
|
34
|
+
|
35
|
+
HostItem.defaultProps = {
|
36
|
+
actions: [],
|
37
|
+
};
|
38
|
+
|
39
|
+
export default HostItem;
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Icon } from 'patternfly-react';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
|
6
|
+
const HostStatus = ({ status }) => {
|
7
|
+
switch (status) {
|
8
|
+
case 'cancelled':
|
9
|
+
return (
|
10
|
+
<div>
|
11
|
+
<Icon type="pf" name="warning-triangle-o" /> {status}
|
12
|
+
</div>
|
13
|
+
);
|
14
|
+
case 'N/A':
|
15
|
+
return (
|
16
|
+
<div>
|
17
|
+
<Icon type="fa" name="question" /> {status}
|
18
|
+
</div>
|
19
|
+
);
|
20
|
+
case 'running':
|
21
|
+
return (
|
22
|
+
<div>
|
23
|
+
<Icon type="pf" name="running" /> {status}
|
24
|
+
</div>
|
25
|
+
);
|
26
|
+
case 'planned':
|
27
|
+
return (
|
28
|
+
<div>
|
29
|
+
<Icon type="pf" name="build" /> {status}
|
30
|
+
</div>
|
31
|
+
);
|
32
|
+
case 'warning':
|
33
|
+
case 'error':
|
34
|
+
return (
|
35
|
+
<div>
|
36
|
+
<Icon type="pf" name="error-circle-o" /> {__('failed')}
|
37
|
+
</div>
|
38
|
+
);
|
39
|
+
case 'success':
|
40
|
+
return (
|
41
|
+
<div>
|
42
|
+
<Icon type="pf" name="ok" /> {status}
|
43
|
+
</div>
|
44
|
+
);
|
45
|
+
default:
|
46
|
+
return <span>{status}</span>;
|
47
|
+
}
|
48
|
+
};
|
49
|
+
|
50
|
+
HostStatus.propTypes = {
|
51
|
+
status: PropTypes.string.isRequired,
|
52
|
+
};
|
53
|
+
|
54
|
+
export default HostStatus;
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import React, { useEffect } from 'react';
|
2
|
+
import { useSelector, useDispatch } from 'react-redux';
|
3
|
+
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
4
|
+
import TargetingHosts from './TargetingHosts';
|
5
|
+
|
6
|
+
import {
|
7
|
+
selectItems,
|
8
|
+
selectStatus,
|
9
|
+
selectAutoRefresh,
|
10
|
+
} from './TargetingHostsSelectors';
|
11
|
+
import { getData } from './TargetingHostsActions';
|
12
|
+
import { TARGETING_HOSTS } from './TargetingHostsConsts';
|
13
|
+
|
14
|
+
const WrappedTargetingHosts = () => {
|
15
|
+
const dispatch = useDispatch();
|
16
|
+
const autoRefresh = useSelector(selectAutoRefresh);
|
17
|
+
const items = useSelector(selectItems);
|
18
|
+
const status = useSelector(selectStatus);
|
19
|
+
|
20
|
+
useEffect(() => {
|
21
|
+
dispatch(getData());
|
22
|
+
|
23
|
+
return () => {
|
24
|
+
dispatch(stopInterval(TARGETING_HOSTS));
|
25
|
+
};
|
26
|
+
}, [dispatch]);
|
27
|
+
|
28
|
+
useEffect(() => {
|
29
|
+
if (autoRefresh === 'false') {
|
30
|
+
dispatch(stopInterval(TARGETING_HOSTS));
|
31
|
+
}
|
32
|
+
}, [autoRefresh, dispatch]);
|
33
|
+
|
34
|
+
return <TargetingHosts status={status} items={items} />;
|
35
|
+
};
|
36
|
+
|
37
|
+
export default WrappedTargetingHosts;
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.3.
|
4
|
+
version: 3.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -90,16 +90,16 @@ dependencies:
|
|
90
90
|
name: rubocop
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
|
-
- - "
|
93
|
+
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version:
|
95
|
+
version: 0.80.0
|
96
96
|
type: :development
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
|
-
- - "
|
100
|
+
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
102
|
+
version: 0.80.0
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: rdoc
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -238,9 +238,6 @@ files:
|
|
238
238
|
- app/views/job_invocations/_card_user_input.html.erb
|
239
239
|
- app/views/job_invocations/_description_fields.html.erb
|
240
240
|
- app/views/job_invocations/_form.html.erb
|
241
|
-
- app/views/job_invocations/_host_actions_td.html.erb
|
242
|
-
- app/views/job_invocations/_host_name_td.html.erb
|
243
|
-
- app/views/job_invocations/_host_status_td.html.erb
|
244
241
|
- app/views/job_invocations/_preview_hosts_list.html.erb
|
245
242
|
- app/views/job_invocations/_preview_hosts_modal.html.erb
|
246
243
|
- app/views/job_invocations/_rerun_taxonomies.html.erb
|
@@ -252,7 +249,7 @@ files:
|
|
252
249
|
- app/views/job_invocations/new.html.erb
|
253
250
|
- app/views/job_invocations/refresh.js.erb
|
254
251
|
- app/views/job_invocations/show.html.erb
|
255
|
-
- app/views/job_invocations/show.
|
252
|
+
- app/views/job_invocations/show.json.erb
|
256
253
|
- app/views/job_invocations/welcome.html.erb
|
257
254
|
- app/views/job_templates/_custom_tab_headers.html.erb
|
258
255
|
- app/views/job_templates/_custom_tabs.html.erb
|
@@ -396,7 +393,24 @@ files:
|
|
396
393
|
- test/unit/renderer_scope_input.rb
|
397
394
|
- test/unit/targeting_test.rb
|
398
395
|
- test/unit/template_invocation_input_value_test.rb
|
396
|
+
- webpack/__mocks__/foremanReact/common/I18n.js
|
397
|
+
- webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js
|
398
|
+
- webpack/__mocks__/foremanReact/constants.js
|
399
399
|
- webpack/index.js
|
400
|
+
- webpack/react_app/components/TargetingHosts/TargetingHosts.js
|
401
|
+
- webpack/react_app/components/TargetingHosts/TargetingHostsActions.js
|
402
|
+
- webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js
|
403
|
+
- webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js
|
404
|
+
- webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js
|
405
|
+
- webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js
|
406
|
+
- webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js
|
407
|
+
- webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap
|
408
|
+
- webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap
|
409
|
+
- webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap
|
410
|
+
- webpack/react_app/components/TargetingHosts/__tests__/fixtures.js
|
411
|
+
- webpack/react_app/components/TargetingHosts/components/HostItem.js
|
412
|
+
- webpack/react_app/components/TargetingHosts/components/HostStatus.js
|
413
|
+
- webpack/react_app/components/TargetingHosts/index.js
|
400
414
|
- webpack/react_app/components/jobInvocations/AggregateStatus/index.js
|
401
415
|
- webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js
|
402
416
|
- webpack/react_app/components/jobInvocations/index.js
|
@@ -1,8 +0,0 @@
|
|
1
|
-
<td class="host_name" id="<%= dom_id(host) %>-name" data-refresh_required="<%= task.nil? ? 'true' : '' %>" data-id="<%= host.id %>">
|
2
|
-
<% if task %>
|
3
|
-
<%= link_to_if_authorized host.name, hash_for_template_invocation_path(:id => template_invocation).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer) %>
|
4
|
-
|
5
|
-
<% else %>
|
6
|
-
<%= host.name %>
|
7
|
-
<% end %>
|
8
|
-
</td>
|
@@ -1 +0,0 @@
|
|
1
|
-
<td class="host_status" id="<%= dom_id(host) %>-status" data-refresh_required="<%= task.nil? || task.pending? ? 'true' : '' %>" data-id="<%= host.id %>"><%= template_invocation_status(task, job_invocation.task) %></td>
|
@@ -1,23 +0,0 @@
|
|
1
|
-
$('div#title_action div.btn-group').html('<%= button_group(job_invocation_task_buttons(@job_invocation.task)).html_safe %>');
|
2
|
-
<% if params[:hosts_needs_refresh] == 'true' && @job_invocation.resolved? %>
|
3
|
-
var hosts_table = $('div#hosts');
|
4
|
-
hosts_table.html('<%=j render('tab_hosts', :job_invocation => @job_invocation, :hosts => @hosts) %>');
|
5
|
-
hosts_table.data('refresh_required', '');
|
6
|
-
<% end %>
|
7
|
-
|
8
|
-
<% ['name', 'status', 'actions', 'provider'].each do |attribute| %>
|
9
|
-
<% if params["host_ids_needing_#{attribute}_update"].present? %>
|
10
|
-
var td_element;
|
11
|
-
<% Host.authorized(:view_hosts, Host).where(:id => params["host_ids_needing_#{attribute}_update"]).each do |host| %>
|
12
|
-
<% template_invocation = @job_invocation.template_invocations.find { |template_invocation| template_invocation.host_id == host.id } %>
|
13
|
-
<% task = template_invocation.try(:run_host_job_task) %>
|
14
|
-
<% options = { :host => host, :task => task, :job_invocation => @job_invocation, :template_invocation => template_invocation } %>
|
15
|
-
td_element= $('#<%= dom_id(host) %>-<%= attribute %>');
|
16
|
-
td_element.replaceWith('<%=j render("host_#{attribute}_td", options) %>');
|
17
|
-
<% end %>
|
18
|
-
<% end %>
|
19
|
-
<% end %>
|
20
|
-
|
21
|
-
<% if @auto_refresh %>
|
22
|
-
delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), job_invocation_refresh_data());
|
23
|
-
<% end %>
|