foreman_remote_execution 3.3.1 → 3.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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 %>
|