foreman_remote_execution 1.5.4 → 1.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/app/controllers/api/v2/job_invocations_controller.rb +22 -12
- data/app/controllers/job_templates_controller.rb +7 -2
- data/app/helpers/remote_execution_helper.rb +7 -1
- data/app/models/input_template_renderer.rb +1 -1
- data/app/models/job_invocation_composer.rb +10 -1
- data/app/models/job_template.rb +4 -2
- data/app/views/job_invocations/_form.html.erb +10 -2
- data/app/views/overrides/subnets/_rex_tab_pane.html.erb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/factories/foreman_remote_execution_factories.rb +5 -7
- data/test/functional/api/v2/job_invocations_controller_test.rb +24 -0
- data/test/functional/api/v2/job_templates_controller_test.rb +1 -0
- data/test/functional/job_templates_controller_test.rb +31 -0
- data/test/unit/job_invocation_composer_test.rb +18 -0
- data/test/unit/job_template_test.rb +24 -3
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9494abeb23995efb0f837561fd1b1bd84fbd7ef37060081ea573d2dde16bd74b
|
4
|
+
data.tar.gz: afbf23769a88fecbbcb7197249b2261a046b568be38699c35a12210ab6cb13e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28dd96a2c75c9e94f71a6613fac13ecbf7938d46fdfc0d5208833073149a3e60c84de6a7ee357ca22318abae29a7deb85afb77377ff462ee71aade08cf7ec0ca
|
7
|
+
data.tar.gz: 5d012baa162db923230e7d7c0f0dc03a315075ce79f3bc64b8cc1236c328061c2ff839f97c56849157c303fc5e758376ecab15d65137cf206aabe6159d7025cf
|
@@ -82,17 +82,15 @@ module Api
|
|
82
82
|
return
|
83
83
|
end
|
84
84
|
task = @nested_obj.sub_task_for_host(@host)
|
85
|
-
refresh =
|
86
|
-
|
85
|
+
refresh = true
|
86
|
+
line_sets = []
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
if line_sets.blank?
|
92
|
-
render :json => {:refresh => refresh, :output => []}
|
93
|
-
else
|
94
|
-
render :json => {:refresh => refresh, :output => line_sets }
|
88
|
+
if task
|
89
|
+
refresh = task.pending?
|
90
|
+
line_sets = output_lines_since task, params[:since]
|
95
91
|
end
|
92
|
+
|
93
|
+
render :json => { :refresh => refresh, :output => line_sets || [] }
|
96
94
|
end
|
97
95
|
|
98
96
|
api :POST, '/job_invocations/:id/cancel', N_('Cancel job invocation')
|
@@ -113,9 +111,14 @@ module Api
|
|
113
111
|
param :failed_only, :bool
|
114
112
|
def rerun
|
115
113
|
composer = JobInvocationComposer.from_job_invocation(@job_invocation, params)
|
116
|
-
composer.
|
117
|
-
|
118
|
-
|
114
|
+
if composer.rerun_possible?
|
115
|
+
composer.trigger!
|
116
|
+
@job_invocation = composer.job_invocation
|
117
|
+
process_response @job_invocation
|
118
|
+
else
|
119
|
+
render :json => { :error => _('Could not rerun job %{id} because its template could not be found') % { :id => composer.reruns } },
|
120
|
+
:status => 404
|
121
|
+
end
|
119
122
|
end
|
120
123
|
|
121
124
|
private
|
@@ -163,6 +166,13 @@ module Api
|
|
163
166
|
job_invocation_params[:inputs].to_hash
|
164
167
|
)
|
165
168
|
end
|
169
|
+
|
170
|
+
def output_lines_since(task, time)
|
171
|
+
since = time.to_f if time.present?
|
172
|
+
line_sets = task.main_action.live_output
|
173
|
+
line_sets = line_sets.drop_while { |o| o['timestamp'].to_f <= since } if since
|
174
|
+
line_sets
|
175
|
+
end
|
166
176
|
end
|
167
177
|
end
|
168
178
|
end
|
@@ -19,10 +19,15 @@ class JobTemplatesController < ::TemplatesController
|
|
19
19
|
host = params[:preview_host_id].present? ? base.find(params[:preview_host_id]) : base.first
|
20
20
|
@template.template = params[:template]
|
21
21
|
renderer = InputTemplateRenderer.new(@template, host)
|
22
|
-
|
22
|
+
output = renderer.preview
|
23
|
+
if output
|
23
24
|
render :plain => output
|
24
25
|
else
|
25
|
-
render
|
26
|
+
render status: :not_acceptable,
|
27
|
+
plain: _(
|
28
|
+
'Problem with previewing the template: %{error}. Note that you must save template input changes before you try to preview it.' %
|
29
|
+
{:error => renderer.error_message}
|
30
|
+
)
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
@@ -161,7 +161,7 @@ module RemoteExecutionHelper
|
|
161
161
|
|
162
162
|
def preview_box(template_invocation, target)
|
163
163
|
renderer = InputTemplateRenderer.new(template_invocation.template, target, template_invocation)
|
164
|
-
if (preview = renderer.preview)
|
164
|
+
if (preview = (load_template_from_task(template_invocation, target) || renderer.preview))
|
165
165
|
content_tag :pre, preview
|
166
166
|
elsif target.nil?
|
167
167
|
alert :text => _('Could not render the preview because no host matches the search query.'),
|
@@ -234,4 +234,10 @@ module RemoteExecutionHelper
|
|
234
234
|
end
|
235
235
|
end
|
236
236
|
end
|
237
|
+
|
238
|
+
def load_template_from_task(template_invocation, target)
|
239
|
+
task = template_invocation.job_invocation.sub_task_for_host(target)
|
240
|
+
return if task.nil?
|
241
|
+
task.execution_plan.actions[1].try(:input) { |input| input['script'] }
|
242
|
+
end
|
237
243
|
end
|
@@ -58,7 +58,7 @@ class InputTemplateRenderer
|
|
58
58
|
def render_template(template_name, input_values = {}, options = {})
|
59
59
|
options.assert_valid_keys(:with_foreign_input_set)
|
60
60
|
with_foreign_input_set = options.fetch(:with_foreign_input_set, true)
|
61
|
-
template = @template.class.authorized("view_#{@template.class.to_s.underscore}").find_by(:name => template_name)
|
61
|
+
template = @template.class.authorized("view_#{@template.class.to_s.underscore.pluralize}").find_by(:name => template_name)
|
62
62
|
unless template
|
63
63
|
self.error_message = _('included template \'%s\' not found') % template_name
|
64
64
|
raise error_message
|
@@ -186,7 +186,8 @@ class JobInvocationComposer
|
|
186
186
|
:concurrency_control => concurrency_control_params,
|
187
187
|
:execution_timeout_interval => job_invocation.execution_timeout_interval,
|
188
188
|
:remote_execution_feature_id => job_invocation.remote_execution_feature_id,
|
189
|
-
:template_invocations => template_invocations_params
|
189
|
+
:template_invocations => template_invocations_params,
|
190
|
+
:reruns => job_invocation.id }.with_indifferent_access
|
190
191
|
end
|
191
192
|
|
192
193
|
private
|
@@ -297,6 +298,7 @@ class JobInvocationComposer
|
|
297
298
|
end
|
298
299
|
|
299
300
|
attr_accessor :params, :job_invocation, :host_ids, :search_query
|
301
|
+
attr_reader :reruns
|
300
302
|
delegate :job_category, :remote_execution_feature_id, :pattern_template_invocations, :template_invocations, :targeting, :triggering, :to => :job_invocation
|
301
303
|
|
302
304
|
def initialize(params, set_defaults = false)
|
@@ -304,6 +306,7 @@ class JobInvocationComposer
|
|
304
306
|
@set_defaults = set_defaults
|
305
307
|
@job_invocation = JobInvocation.new
|
306
308
|
@job_invocation.task_group = JobInvocationTaskGroup.new
|
309
|
+
@reruns = params[:reruns]
|
307
310
|
compose
|
308
311
|
|
309
312
|
@host_ids = validate_host_ids(params[:host_ids])
|
@@ -342,6 +345,8 @@ class JobInvocationComposer
|
|
342
345
|
job_invocation.key_passphrase = params[:key_passphrase]
|
343
346
|
job_invocation.sudo_password = params[:sudo_password]
|
344
347
|
|
348
|
+
job_invocation.job_category = nil unless rerun_possible?
|
349
|
+
|
345
350
|
self
|
346
351
|
end
|
347
352
|
# rubocop:enable Metrics/AbcSize
|
@@ -470,6 +475,10 @@ class JobInvocationComposer
|
|
470
475
|
job_invocation.pattern_template_invocations.map(&:template_id)
|
471
476
|
end
|
472
477
|
|
478
|
+
def rerun_possible?
|
479
|
+
!(reruns && job_invocation.pattern_template_invocations.empty?)
|
480
|
+
end
|
481
|
+
|
473
482
|
private
|
474
483
|
|
475
484
|
# builds input values for a given templates id based on params
|
data/app/models/job_template.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
class JobTemplate < ::Template
|
2
2
|
audited
|
3
3
|
include ::Exportable
|
4
|
+
include ::TemplateTax
|
4
5
|
|
5
6
|
class NonUniqueInputsError < Foreman::Exception
|
6
7
|
end
|
7
8
|
|
8
9
|
attr_exportable :job_category, :description_format, :template_inputs,
|
9
|
-
:foreign_input_sets, :provider_type,
|
10
|
+
:foreign_input_sets, :provider_type,
|
11
|
+
{ :kind => ->(template) { template.class.name.underscore } }.merge(taxonomy_exportable)
|
10
12
|
|
11
13
|
include Authorizable
|
12
14
|
extend FriendlyId
|
@@ -134,7 +136,7 @@ class JobTemplate < ::Template
|
|
134
136
|
|
135
137
|
def generate_description_format
|
136
138
|
if description_format.blank?
|
137
|
-
generated_description = '%{
|
139
|
+
generated_description = '%{template_name}'
|
138
140
|
unless template_inputs_with_foreign.empty?
|
139
141
|
inputs = template_inputs_with_foreign.map(&:name).map { |name| %{#{name}="%{#{name}}"} }.join(' ')
|
140
142
|
generated_description << " with inputs #{inputs}"
|
@@ -6,7 +6,15 @@
|
|
6
6
|
|
7
7
|
<%= form_for @composer.job_invocation, :html => {'data-refresh-url' => refresh_job_invocations_path, :id => 'job_invocation_form'} do |f| %>
|
8
8
|
|
9
|
-
|
9
|
+
<% unless @composer.rerun_possible? %>
|
10
|
+
<%= alert :text => _('Could not rerun job %{id} because its template could not be found') % { :id => @composer.reruns },
|
11
|
+
:class => 'alert alert-block alert-danger has-error',
|
12
|
+
:close => false %>
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
<%= selectable_f f, :job_category, @composer.available_job_categories,
|
16
|
+
{ :selected => @composer.job_invocation.job_category, :include_blank => @composer.job_invocation.job_category.nil? },
|
17
|
+
:label => _('Job category') %>
|
10
18
|
<%= f.hidden_field(:remote_execution_feature_id, :value => @composer.remote_execution_feature_id) %>
|
11
19
|
|
12
20
|
<% selected_templates_per_provider = {} %>
|
@@ -116,5 +124,5 @@
|
|
116
124
|
|
117
125
|
<%= render :partial => 'preview_hosts_modal' %>
|
118
126
|
|
119
|
-
<%= submit_or_cancel f, false, :cancel_path => job_invocations_path %>
|
127
|
+
<%= submit_or_cancel f, false, :cancel_path => job_invocations_path, :disabled => !@composer.rerun_possible? %>
|
120
128
|
<% end %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="tab-pane" id="rex_proxies">
|
2
2
|
<%= fields_for :subnet do |f| %>
|
3
|
-
<%= multiple_selects f, :remote_execution_proxies, SmartProxy.authorized.with_features(*RemoteExecutionProvider.provider_names), @subnet.remote_execution_proxy_ids, {:label => _("Proxies"), :help_inline => _("Select as many remote execution proxies as applicable for this subnet. When multiple proxies with the same provider are added, actions will be load balanced among them.")} %>
|
3
|
+
<%= multiple_selects f, :remote_execution_proxies, SmartProxy.authorized.with_features(*RemoteExecutionProvider.provider_names).distinct, @subnet.remote_execution_proxy_ids, {:label => _("Proxies"), :help_inline => _("Select as many remote execution proxies as applicable for this subnet. When multiple proxies with the same provider are added, actions will be load balanced among them.")} %>
|
4
4
|
<% end %>
|
5
5
|
</div>
|
@@ -1,14 +1,12 @@
|
|
1
1
|
FactoryBot.define do
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
f.provider_type 'SSH'
|
2
|
+
factory :job_template do
|
3
|
+
sequence(:name) { |n| "Job template #{n}" }
|
4
|
+
sequence(:job_category) { |n| "Job name #{n}" }
|
5
|
+
template 'id'
|
6
|
+
provider_type 'SSH'
|
8
7
|
organizations { [Organization.find_by(name: 'Organization 1')] } if SETTINGS[:organizations_enabled]
|
9
8
|
locations { [Location.find_by(name: 'Location 1')] } if SETTINGS[:locations_enabled]
|
10
9
|
|
11
|
-
|
12
10
|
trait :with_input do
|
13
11
|
after(:build) do |template, evaluator|
|
14
12
|
template.template_inputs << FactoryBot.build(:template_input)
|
@@ -6,6 +6,11 @@ module Api
|
|
6
6
|
setup do
|
7
7
|
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
8
8
|
@template = FactoryBot.create(:job_template, :with_input)
|
9
|
+
|
10
|
+
# Without this the template in template_invocations and in pattern_template_invocations
|
11
|
+
# would belong to different job_categories, causing trouble when trying to rerun
|
12
|
+
@invocation.job_category = @invocation.pattern_template_invocations.first.template.job_category
|
13
|
+
@invocation.save!
|
9
14
|
end
|
10
15
|
|
11
16
|
test 'should get index' do
|
@@ -107,6 +112,16 @@ module Api
|
|
107
112
|
assert_response :success
|
108
113
|
end
|
109
114
|
|
115
|
+
test 'should provide empty output for host which does not have a task yet' do
|
116
|
+
host = @invocation.template_invocations_hosts.first
|
117
|
+
JobInvocation.any_instance.expects(:sub_task_for_host).returns(nil)
|
118
|
+
get :output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
119
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
120
|
+
assert_equal result['refresh'], true
|
121
|
+
assert_equal result['output'], []
|
122
|
+
assert_response :success
|
123
|
+
end
|
124
|
+
|
110
125
|
test 'should cancel a job' do
|
111
126
|
@invocation.task.expects(:cancellable?).returns(true)
|
112
127
|
@invocation.task.expects(:cancel).returns(true)
|
@@ -168,6 +183,8 @@ module Api
|
|
168
183
|
|
169
184
|
test 'should rerun failed only' do
|
170
185
|
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_failed_task)
|
186
|
+
@invocation.job_category = @invocation.pattern_template_invocations.first.template.job_category
|
187
|
+
@invocation.save!
|
171
188
|
JobInvocation.any_instance.expects(:generate_description)
|
172
189
|
JobInvocationComposer.any_instance
|
173
190
|
.expects(:validate_job_category)
|
@@ -181,6 +198,13 @@ module Api
|
|
181
198
|
targeting.user_id.must_equal users(:admin).id
|
182
199
|
targeting.search_query.must_equal "name ^ (#{hostnames.join(',')})"
|
183
200
|
end
|
201
|
+
|
202
|
+
test 'should return 404 if template is not found' do
|
203
|
+
@invocation.job_category = 'Missing category'
|
204
|
+
@invocation.save!
|
205
|
+
post :rerun, params: { :id => @invocation.id }
|
206
|
+
assert_response 404
|
207
|
+
end
|
184
208
|
end
|
185
209
|
end
|
186
210
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_plugin_helper'
|
4
|
+
|
5
|
+
class JobTemplatesControllerTest < ActionController::TestCase
|
6
|
+
context '#preview' do
|
7
|
+
let(:template) { FactoryBot.create(:job_template) }
|
8
|
+
let(:host) { FactoryBot.create(:host, :managed) }
|
9
|
+
|
10
|
+
test 'should render a preview version of a template' do
|
11
|
+
post :preview, params: { job_template: template.to_param, template: 'uptime' }, session: set_session_user
|
12
|
+
assert_response :success
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'should render a preview version of a template for a specific host' do
|
16
|
+
post :preview, params: {
|
17
|
+
job_template: template.to_param,
|
18
|
+
template: '<%= @host.name %>',
|
19
|
+
preview_host_id: host.id
|
20
|
+
}, session: set_session_user
|
21
|
+
assert_response :success
|
22
|
+
assert_equal host.name, @response.body
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'should render a error message when template has errors' do
|
26
|
+
InputTemplateRenderer.any_instance.stubs(:render).returns(false)
|
27
|
+
post :preview, params: { job_template: template.to_param }, session: set_session_user
|
28
|
+
assert_response :not_acceptable
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -156,6 +156,24 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
|
|
156
156
|
end
|
157
157
|
end
|
158
158
|
|
159
|
+
describe '#rerun_possible?' do
|
160
|
+
it 'is true when not rerunning' do
|
161
|
+
composer.must_be :rerun_possible?
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'is true when rerunning with pattern tempalte invocations' do
|
165
|
+
composer.expects(:reruns).returns(1)
|
166
|
+
composer.job_invocation.expects(:pattern_template_invocations).returns([1])
|
167
|
+
composer.must_be :rerun_possible?
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'is false when rerunning without pattern template invocations' do
|
171
|
+
composer.expects(:reruns).returns(1)
|
172
|
+
composer.job_invocation.expects(:pattern_template_invocations).returns([])
|
173
|
+
composer.wont_be :rerun_possible?
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
159
177
|
describe '#selected_job_templates' do
|
160
178
|
it 'returns no template if none was selected through params' do
|
161
179
|
composer.selected_job_templates.must_be_empty
|
@@ -44,14 +44,14 @@ class JobTemplateTest < ActiveSupport::TestCase
|
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'uses the job name as description_format if not set or blank and has no inputs' do
|
47
|
-
minimal_template.generate_description_format.must_equal '%{
|
47
|
+
minimal_template.generate_description_format.must_equal '%{template_name}'
|
48
48
|
minimal_template.description_format = ''
|
49
|
-
minimal_template.generate_description_format.must_equal '%{
|
49
|
+
minimal_template.generate_description_format.must_equal '%{template_name}'
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'generates the description_format if not set or blank and has inputs' do
|
53
53
|
input_name = template.template_inputs.first.name
|
54
|
-
expected_result = %(%{
|
54
|
+
expected_result = %(%{template_name} with inputs #{input_name}="%{#{input_name}}")
|
55
55
|
template.generate_description_format.must_equal expected_result
|
56
56
|
template.description_format = ''
|
57
57
|
template.generate_description_format.must_equal expected_result
|
@@ -267,6 +267,11 @@ class JobTemplateTest < ActiveSupport::TestCase
|
|
267
267
|
imported.name.must_equal old_name
|
268
268
|
imported.template_inputs.first.to_export.must_equal exportable_template.template_inputs.first.to_export
|
269
269
|
end
|
270
|
+
|
271
|
+
it 'has taxonomies in metadata' do
|
272
|
+
assert_equal 'Organization 1', exportable_template.to_export["organizations"].first
|
273
|
+
assert_equal 'Location 1', exportable_template.to_export["locations"].first
|
274
|
+
end
|
270
275
|
end
|
271
276
|
|
272
277
|
context 'there is existing template invocation of a job template' do
|
@@ -290,4 +295,20 @@ class JobTemplateTest < ActiveSupport::TestCase
|
|
290
295
|
refute_valid job_template
|
291
296
|
end
|
292
297
|
end
|
298
|
+
|
299
|
+
context 'rendering' do
|
300
|
+
it 'renders nested template as a non-admin user' do
|
301
|
+
inner = FactoryBot.create(:job_template)
|
302
|
+
template_invocation = FactoryBot.create(:template_invocation)
|
303
|
+
template_invocation.template.template = "<wrap><%= render_template('#{inner.name}') %></wrap>"
|
304
|
+
template_invocation.template.save!
|
305
|
+
|
306
|
+
setup_user('view', 'job_templates')
|
307
|
+
renderer = InputTemplateRenderer.new template_invocation.template,
|
308
|
+
template_invocation.host,
|
309
|
+
template_invocation
|
310
|
+
result = renderer.render
|
311
|
+
result.must_equal "<wrap>#{inner.template}</wrap>"
|
312
|
+
end
|
313
|
+
end
|
293
314
|
end
|
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: 1.5.
|
4
|
+
version: 1.5.5
|
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: 2018-
|
11
|
+
date: 2018-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -368,6 +368,7 @@ files:
|
|
368
368
|
- test/functional/api/v2/template_inputs_controller_test.rb
|
369
369
|
- test/functional/api/v2/template_invocations_controller_test.rb
|
370
370
|
- test/functional/job_invocations_controller_test.rb
|
371
|
+
- test/functional/job_templates_controller_test.rb
|
371
372
|
- test/test_plugin_helper.rb
|
372
373
|
- test/unit/actions/run_host_job_test.rb
|
373
374
|
- test/unit/actions/run_hosts_job_test.rb
|
@@ -416,7 +417,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
416
417
|
version: '0'
|
417
418
|
requirements: []
|
418
419
|
rubyforge_project:
|
419
|
-
rubygems_version: 2.
|
420
|
+
rubygems_version: 2.7.3
|
420
421
|
signing_key:
|
421
422
|
specification_version: 4
|
422
423
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
@@ -432,6 +433,7 @@ test_files:
|
|
432
433
|
- test/functional/api/v2/template_inputs_controller_test.rb
|
433
434
|
- test/functional/api/v2/template_invocations_controller_test.rb
|
434
435
|
- test/functional/job_invocations_controller_test.rb
|
436
|
+
- test/functional/job_templates_controller_test.rb
|
435
437
|
- test/test_plugin_helper.rb
|
436
438
|
- test/unit/actions/run_host_job_test.rb
|
437
439
|
- test/unit/actions/run_hosts_job_test.rb
|