foreman_remote_execution 4.0.0 → 4.2.3
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/app/controllers/api/v2/job_invocations_controller.rb +20 -2
- data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +26 -0
- data/app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb +21 -0
- data/app/controllers/job_templates_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +3 -2
- data/app/lib/actions/remote_execution/run_hosts_job.rb +11 -2
- data/app/models/concerns/api/v2/interfaces_controller_extensions.rb +13 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +38 -14
- data/app/models/foreign_input_set.rb +1 -1
- data/app/models/job_invocation.rb +6 -1
- data/app/models/job_invocation_composer.rb +1 -1
- data/app/models/remote_execution_feature.rb +5 -2
- data/app/models/remote_execution_provider.rb +6 -1
- data/app/services/remote_execution_proxy_selector.rb +3 -0
- data/app/views/api/v2/interfaces/execution_flag.json.rabl +1 -0
- data/app/views/api/v2/job_invocations/base.json.rabl +1 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
- data/app/views/api/v2/registration/_form.html.erb +12 -0
- data/app/views/api/v2/subnets/remote_execution_proxies.json.rabl +3 -0
- data/app/views/job_invocations/_tab_overview.html.erb +13 -1
- data/app/views/template_invocations/_output_line_set.html.erb +1 -1
- data/app/views/template_invocations/show.html.erb +30 -23
- data/app/views/templates/ssh/package_action.erb +1 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20200820122057_add_proxy_selector_override_to_remote_execution_feature.rb +5 -0
- data/lib/foreman_remote_execution/engine.rb +24 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +29 -0
- data/test/functional/api/v2/registration_controller_test.rb +82 -0
- data/test/unit/actions/run_hosts_job_test.rb +2 -1
- data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +5 -1
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +5 -1
- metadata +15 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a9f5c20b58b5911c612164cc806d63b3096c81506b18cd11423fb4b073bdec4
|
4
|
+
data.tar.gz: 2c8615b8ef06bf5d5adc3e62ab2a36906714ebe9e99adbf13dfb3eaa742819d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 549daa0a07ac47a7ed596187fd86943d9e1e517dab1d653af8f454ccee9e2b83fa85b6e2ec188201e5f849978b50b69108a0dbf6a6c67844b909de3b1b1e846f
|
7
|
+
data.tar.gz: b6e71fedfb385b05aa488fd65b68c2c242eba84783da007e26a6cf5f5b1b837de7bcbcfeecc72459b4c48cee93513f45948aa14318bbc548a52f159f2ae967cd
|
@@ -6,7 +6,7 @@ module Api
|
|
6
6
|
|
7
7
|
before_action :find_optional_nested_object, :only => %w{output raw_output}
|
8
8
|
before_action :find_host, :only => %w{output raw_output}
|
9
|
-
before_action :find_resource, :only => %w{show update destroy clone cancel rerun}
|
9
|
+
before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs}
|
10
10
|
|
11
11
|
wrap_parameters JobInvocation, :include => (JobInvocation.attribute_names + [:ssh])
|
12
12
|
|
@@ -137,6 +137,24 @@ module Api
|
|
137
137
|
end
|
138
138
|
end
|
139
139
|
|
140
|
+
api :GET, '/job_invocations/:id/outputs', N_('Get outputs of hosts in a job')
|
141
|
+
param :id, :identifier, :required => true
|
142
|
+
param :search_query, :identifier, :required => false
|
143
|
+
param :since, String, :required => false
|
144
|
+
param :raw, String, :required => false
|
145
|
+
def outputs
|
146
|
+
hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
147
|
+
hosts = hosts.search_for(params['search_query']) if params['search_query']
|
148
|
+
raw = ActiveRecord::Type::Boolean.new.cast params['raw']
|
149
|
+
default_value = raw ? '' : []
|
150
|
+
outputs = hosts.map do |host|
|
151
|
+
host_output(@job_invocation, host, :default => default_value, :since => params['since'], :raw => raw)
|
152
|
+
.merge(host_id: host.id)
|
153
|
+
end
|
154
|
+
|
155
|
+
render :json => { :outputs => outputs }
|
156
|
+
end
|
157
|
+
|
140
158
|
private
|
141
159
|
|
142
160
|
def allowed_nested_id
|
@@ -145,7 +163,7 @@ module Api
|
|
145
163
|
|
146
164
|
def action_permission
|
147
165
|
case params[:action]
|
148
|
-
when 'output', 'raw_output'
|
166
|
+
when 'output', 'raw_output', 'outputs'
|
149
167
|
:view
|
150
168
|
when 'cancel'
|
151
169
|
:cancel
|
data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module ForemanRemoteExecution
|
2
|
+
module Concerns
|
3
|
+
module Api::V2::RegistrationControllerExtensions
|
4
|
+
module ApipieExtensions
|
5
|
+
extend Apipie::DSL::Concern
|
6
|
+
|
7
|
+
update_api(:global, :host) do
|
8
|
+
param :remote_execution_interface, String, desc: N_("Identifier of the Host interface for Remote execution")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
def host_setup_extension
|
15
|
+
remote_execution_interface
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def remote_execution_interface
|
20
|
+
return unless params['remote_execution_interface'].present?
|
21
|
+
|
22
|
+
@host.set_execution_interface(params['remote_execution_interface'])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module ForemanRemoteExecution
|
2
|
+
module Concerns
|
3
|
+
module Api::V2::SubnetsControllerExtensions
|
4
|
+
module ApiPieExtensions
|
5
|
+
extend ::Apipie::DSL::Concern
|
6
|
+
|
7
|
+
update_api(:create, :update) do
|
8
|
+
param :subnet, Hash do
|
9
|
+
param :remote_execution_proxy_ids, Array, _('List of proxy IDs to be used for remote execution')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
include ApiPieExtensions
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -36,7 +36,7 @@ class JobTemplatesController < ::TemplatesController
|
|
36
36
|
|
37
37
|
@template = JobTemplate.import_raw(contents, :update => Foreman::Cast.to_bool(params[:imported_template][:overwrite]))
|
38
38
|
if @template&.save
|
39
|
-
flash[:
|
39
|
+
flash[:success] = _('Job template imported successfully.')
|
40
40
|
redirect_to job_templates_path(:search => "name = \"#{@template.name}\"")
|
41
41
|
else
|
42
42
|
@template ||= JobTemplate.import_raw(contents, :build_new => true)
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal:true
|
2
2
|
|
3
3
|
module JobInvocationsHelper
|
4
|
-
def minicard(icon, number, text)
|
4
|
+
def minicard(icon, number, text, tooltip: nil)
|
5
|
+
tooltip_options = tooltip ? { :'data-original-title' => tooltip, :rel => 'twipsy' } : {}
|
5
6
|
content_tag(:div, :class => 'card-pf card-pf-accented
|
6
7
|
card-pf-aggregate-status card-pf-aggregate-status-mini') do
|
7
|
-
content_tag(:h2, :class => 'card-pf-title', :style => 'line-height: 1.1') do
|
8
|
+
content_tag(:h2, { :class => 'card-pf-title', :style => 'line-height: 1.1' }.merge(tooltip_options)) do
|
8
9
|
icon_text(icon, '', :kind => 'pficon') +
|
9
10
|
content_tag(:span, number, :class =>'card-pf-aggregate-status-count') +
|
10
11
|
text
|
@@ -57,15 +57,24 @@ module Actions
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def job_invocation
|
60
|
-
|
60
|
+
id = input[:job_invocation_id] || input.fetch(:job_invocation, {})[:id]
|
61
|
+
@job_invocation ||= JobInvocation.find(id)
|
61
62
|
end
|
62
63
|
|
63
64
|
def batch(from, size)
|
64
65
|
hosts.offset(from).limit(size)
|
65
66
|
end
|
66
67
|
|
68
|
+
def initiate
|
69
|
+
output[:host_count] = total_count
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
67
73
|
def total_count
|
68
|
-
|
74
|
+
# For compatibility with already existing tasks
|
75
|
+
return output[:total_count] unless output.has_key?(:host_count) || task.pending?
|
76
|
+
|
77
|
+
output[:host_count] || hosts.count
|
69
78
|
end
|
70
79
|
|
71
80
|
def hosts
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Api
|
2
|
+
module V2
|
3
|
+
module InterfacesControllerExtensions
|
4
|
+
extend Apipie::DSL::Concern
|
5
|
+
|
6
|
+
update_api(:create, :update) do
|
7
|
+
param :interface, Hash do
|
8
|
+
param :execution, :bool, :desc => N_('Should this interface be used for remote execution?')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -44,29 +44,33 @@ module ForemanRemoteExecution
|
|
44
44
|
@execution_status_label ||= get_status(HostStatus::ExecutionStatus).to_label(options)
|
45
45
|
end
|
46
46
|
|
47
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
47
48
|
def host_params_hash
|
48
|
-
|
49
|
-
keys = remote_execution_ssh_keys
|
50
|
-
source = 'global'
|
51
|
-
if keys.present?
|
52
|
-
value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
|
53
|
-
params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source}
|
54
|
-
end
|
55
|
-
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
56
|
-
:remote_execution_connect_by_ip].each do |key|
|
57
|
-
value = Setting[key]
|
58
|
-
params[key.to_s] = {:value => value, :safe_value => value, :source => source} unless params.key?(key.to_s)
|
59
|
-
end
|
60
|
-
params
|
49
|
+
@cached_rex_host_params_hash ||= extend_host_params_hash(super)
|
61
50
|
end
|
51
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
62
52
|
|
63
53
|
def execution_interface
|
64
54
|
get_interface_by_flag(:execution)
|
65
55
|
end
|
66
56
|
|
57
|
+
def set_execution_interface(identifier)
|
58
|
+
if interfaces.find_by(identifier: identifier).nil?
|
59
|
+
msg = (N_("Interface with the '%s' identifier was specified as a remote execution interface, however the interface was not found on the host. If the interface exists, it needs to be created in Foreman during the registration.") % identifier)
|
60
|
+
raise ActiveRecord::RecordNotFound, msg
|
61
|
+
end
|
62
|
+
|
63
|
+
# Only one interface at time can be used for REX, all other must be set to false
|
64
|
+
interfaces.each { |int| int.execution = (int.identifier == identifier) }
|
65
|
+
interfaces.each(&:save!)
|
66
|
+
end
|
67
|
+
|
67
68
|
def remote_execution_proxies(provider, authorized = true)
|
68
69
|
proxies = {}
|
69
|
-
proxies[:subnet]
|
70
|
+
proxies[:subnet] = []
|
71
|
+
proxies[:subnet] += execution_interface.subnet6.remote_execution_proxies.with_features(provider) if execution_interface&.subnet6
|
72
|
+
proxies[:subnet] += execution_interface.subnet.remote_execution_proxies.with_features(provider) if execution_interface&.subnet
|
73
|
+
proxies[:subnet].uniq!
|
70
74
|
proxies[:fallback] = smart_proxies.with_features(provider) if Setting[:remote_execution_fallback_proxy]
|
71
75
|
|
72
76
|
if Setting[:remote_execution_global_proxy]
|
@@ -102,8 +106,28 @@ module ForemanRemoteExecution
|
|
102
106
|
super(*args)
|
103
107
|
end
|
104
108
|
|
109
|
+
def clear_host_parameters_cache!
|
110
|
+
super
|
111
|
+
@cached_rex_host_params_hash = nil
|
112
|
+
end
|
113
|
+
|
105
114
|
private
|
106
115
|
|
116
|
+
def extend_host_params_hash(params)
|
117
|
+
keys = remote_execution_ssh_keys
|
118
|
+
source = 'global'
|
119
|
+
if keys.present?
|
120
|
+
value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
|
121
|
+
params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source}
|
122
|
+
end
|
123
|
+
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
124
|
+
:remote_execution_connect_by_ip].each do |key|
|
125
|
+
value = Setting[key]
|
126
|
+
params[key.to_s] = {:value => value, :safe_value => value, :source => source} unless params.key?(key.to_s)
|
127
|
+
end
|
128
|
+
params
|
129
|
+
end
|
130
|
+
|
107
131
|
def build_required_interfaces(attrs = {})
|
108
132
|
super(attrs)
|
109
133
|
self.primary_interface.execution = true if self.execution_interface.nil?
|
@@ -4,7 +4,7 @@ class ForeignInputSet < ApplicationRecord
|
|
4
4
|
class CircularDependencyError < Foreman::Exception
|
5
5
|
end
|
6
6
|
|
7
|
-
attr_exportable :exclude, :include, :include_all, :template => ->(input_set) { input_set.
|
7
|
+
attr_exportable :exclude, :include, :include_all, :template => ->(input_set) { input_set.target_template.name }
|
8
8
|
|
9
9
|
belongs_to :template
|
10
10
|
belongs_to :target_template, :class_name => 'Template'
|
@@ -22,6 +22,7 @@ class JobInvocation < ApplicationRecord
|
|
22
22
|
validates :job_category, :presence => true
|
23
23
|
validates_associated :targeting, :all_template_invocations
|
24
24
|
|
25
|
+
scoped_search :on => :id, :complete_value => true
|
25
26
|
scoped_search :on => :job_category, :complete_value => true
|
26
27
|
scoped_search :on => :description, :complete_value => true
|
27
28
|
|
@@ -173,7 +174,7 @@ class JobInvocation < ApplicationRecord
|
|
173
174
|
|
174
175
|
def total_hosts_count
|
175
176
|
if targeting.resolved?
|
176
|
-
targeting.hosts.count
|
177
|
+
task&.main_action&.total_count || targeting.hosts.count
|
177
178
|
else
|
178
179
|
_('N/A')
|
179
180
|
end
|
@@ -243,6 +244,10 @@ class JobInvocation < ApplicationRecord
|
|
243
244
|
!task.pending?
|
244
245
|
end
|
245
246
|
|
247
|
+
def missing_hosts_count
|
248
|
+
targeting.resolved? ? total_hosts_count - targeting.hosts.count : 0
|
249
|
+
end
|
250
|
+
|
246
251
|
private
|
247
252
|
|
248
253
|
def failed_template_invocations
|
@@ -482,7 +482,7 @@ class JobInvocationComposer
|
|
482
482
|
|
483
483
|
def input_value_for(input)
|
484
484
|
invocations = pattern_template_invocations
|
485
|
-
default = TemplateInvocationInputValue.new
|
485
|
+
default = TemplateInvocationInputValue.new(:template_input_id => input.id)
|
486
486
|
invocations.map(&:input_values).flatten.detect { |iv| iv.template_input_id == input.id } || default
|
487
487
|
end
|
488
488
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
class RemoteExecutionFeature < ApplicationRecord
|
2
|
-
VALID_OPTIONS = [:provided_inputs, :description, :host_action_button, :notification_builder].freeze
|
2
|
+
VALID_OPTIONS = [:provided_inputs, :description, :host_action_button, :notification_builder, :proxy_selector_override].freeze
|
3
3
|
validates :label, :name, :presence => true, :uniqueness => true
|
4
4
|
|
5
5
|
belongs_to :job_template
|
@@ -24,8 +24,10 @@ class RemoteExecutionFeature < ApplicationRecord
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.register(label, name, options = {})
|
27
|
+
pending_migrations = ::Foreman::Plugin.registered_plugins[:foreman_remote_execution]&.pending_migrations
|
27
28
|
begin
|
28
|
-
|
29
|
+
# Let's not try to register features if rex is not registered as a plugin
|
30
|
+
return false if pending_migrations || pending_migrations.nil?
|
29
31
|
rescue ActiveRecord::NoDatabaseError => e
|
30
32
|
# just ignore the problem if DB does not exist yet (rake db:create call)
|
31
33
|
return false
|
@@ -41,6 +43,7 @@ class RemoteExecutionFeature < ApplicationRecord
|
|
41
43
|
:provided_input_names => options[:provided_inputs],
|
42
44
|
:description => options[:description],
|
43
45
|
:host_action_button => options[:host_action_button],
|
46
|
+
:proxy_selector_override => options[:proxy_selector_override],
|
44
47
|
:notification_builder => builder }
|
45
48
|
# in case DB does not have the attribute created yet but plugin initializer registers the feature, we need to skip this attribute
|
46
49
|
attrs = [ :host_action_button, :notification_builder ]
|
@@ -101,7 +101,12 @@ class RemoteExecutionProvider
|
|
101
101
|
|
102
102
|
# Return a specific proxy selector to use for running a given template
|
103
103
|
# Returns either nil to use the default selector or an instance of a (sub)class of ::ForemanTasks::ProxySelector
|
104
|
-
def required_proxy_selector_for(
|
104
|
+
def required_proxy_selector_for(template)
|
105
|
+
if template.remote_execution_features
|
106
|
+
.where(:proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY)
|
107
|
+
.any?
|
108
|
+
::DefaultProxyProxySelector.new
|
109
|
+
end
|
105
110
|
end
|
106
111
|
end
|
107
112
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
attributes :execution
|
@@ -8,6 +8,7 @@ node do |invocation|
|
|
8
8
|
:failed => invocation_count(invocation, :output_key => :failed_count),
|
9
9
|
:pending => invocation_count(invocation, :output_key => :pending_count),
|
10
10
|
:total => invocation_count(invocation, :output_key => :total_count),
|
11
|
+
:missing => invocation.missing_hosts_count,
|
11
12
|
}
|
12
13
|
end
|
13
14
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<div class='form-group'>
|
2
|
+
<label class='col-md-2 control-label'>
|
3
|
+
<%= _('Remote Execution Interface') %>
|
4
|
+
<% help = _('Identifier of the Host interface for Remote execution') %>
|
5
|
+
<a rel="popover" data-content="<%= help %>" data-trigger="focus" data-container="body" data-html="true" tabindex="-1">
|
6
|
+
<span class="pficon pficon-info "></span>
|
7
|
+
</a>
|
8
|
+
</label>
|
9
|
+
<div class='col-md-4'>
|
10
|
+
<%= text_field_tag 'remote_execution_interface', params[:remote_execution_interface], class: 'form-control' %>
|
11
|
+
</div>
|
12
|
+
</div>
|
@@ -9,7 +9,19 @@
|
|
9
9
|
<% template_invocations.each do |template_invocation| %>
|
10
10
|
<%= minicard('user', template_invocation.effective_user || Setting[:remote_execution_effective_user],
|
11
11
|
template_invocation.template.name + ' ' + _('effective user')) %>
|
12
|
-
|
12
|
+
<div class="row">
|
13
|
+
<% missing = job_invocation.missing_hosts_count %>
|
14
|
+
<% size = missing.zero? ? 12 : 6 %>
|
15
|
+
<div class="col-xs-12 col-sm-<%= size %> col-md-<%= size %>" >
|
16
|
+
<%= minicard('cluster', job_invocation.total_hosts_count, _('Total hosts')) %>
|
17
|
+
</div>
|
18
|
+
<% unless missing.zero? %>
|
19
|
+
<div class="col-xs-12 col-sm-6 col-md-6" >
|
20
|
+
<%= minicard('warning-triangle-o', missing, _('Hosts gone missing'),
|
21
|
+
:tooltip => _('This can happen if the host is removed or moved to another organization or location after the job was started')) %>
|
22
|
+
</div>
|
23
|
+
<% end %>
|
24
|
+
</div>
|
13
25
|
<% if template_invocation.input_values.present? %>
|
14
26
|
<%= render :partial => 'card_user_input', :locals => { :template_invocation => template_invocation } %>
|
15
27
|
<% end %>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<% output_line_set['output'].gsub("\r\n", "\n").sub(/\n\Z/, '').split("\n", -1).each do |line| %>
|
2
2
|
<%= content_tag :div, :class => 'line ' + output_line_set['output_type'], :data => { :timestamp => output_line_set['timestamp'] } do %>
|
3
3
|
|
4
|
-
<%= content_tag(:span, (@line_counter += 1).to_s.rjust(4).gsub(' ', ' ').html_safe + ':', :class => 'counter', :title => (output_line_set['timestamp'] && Time.at(output_line_set['timestamp']))) %>
|
4
|
+
<%= content_tag(:span, (@line_counter += 1).to_s.rjust(4).gsub(' ', ' ').html_safe + ':', :class => 'counter', :title => (output_line_set['timestamp'] && Time.at(output_line_set['timestamp'].to_f))) %>
|
5
5
|
<%= content_tag(:div, colorize_line(line.gsub(JobInvocationOutputHelper::COLOR_PATTERN, '').empty? ? "#{line}\n" : line).html_safe, :class => 'content') %>
|
6
6
|
<% end %>
|
7
7
|
<% end %>
|
@@ -1,12 +1,16 @@
|
|
1
1
|
<% items = [{ :caption => _('Job invocations'), :url => job_invocations_path },
|
2
2
|
{ :caption => @template_invocation.job_invocation.description,
|
3
|
-
:url => job_invocation_path(@template_invocation.job_invocation_id) }
|
4
|
-
{ :caption => _('Template Invocation for %s') % @template_invocation.host.name }] %>
|
3
|
+
:url => job_invocation_path(@template_invocation.job_invocation_id) }]
|
5
4
|
|
6
|
-
|
5
|
+
if @host
|
6
|
+
items << { :caption => _('Template Invocation for %s') % @host.name }
|
7
|
+
breadcrumbs(:resource_url => template_invocations_api_job_invocation_path(@template_invocation.job_invocation_id),
|
7
8
|
:name_field => 'host_name',
|
8
9
|
:switcher_item_url => template_invocation_path(':id'),
|
9
10
|
:items => items)
|
11
|
+
else
|
12
|
+
breadcrumbs(items: items, switchable: false)
|
13
|
+
end
|
10
14
|
%>
|
11
15
|
|
12
16
|
<% stylesheet 'foreman_remote_execution/foreman_remote_execution' %>
|
@@ -18,31 +22,34 @@
|
|
18
22
|
<%= button_group(link_to_function(_('Toggle command'), '$("div.preview").toggle()', :class => 'btn btn-default'),
|
19
23
|
link_to_function(_('Toggle STDERR'), '$("div.line.stderr").toggle()', :class => 'btn btn-default'),
|
20
24
|
link_to_function(_('Toggle STDOUT'), '$("div.line.stdout").toggle()', :class => 'btn btn-default'),
|
21
|
-
link_to_function(_('Toggle DEBUG'), '$("div.line.debug").toggle()', :class => 'btn btn-default')) %>
|
25
|
+
link_to_function(_('Toggle DEBUG'), '$("div.line.debug").toggle()', :class => 'btn btn-default')) if @host %>
|
22
26
|
<%= button_group(template_invocation_task_buttons(@template_invocation_task, @template_invocation.job_invocation)) %>
|
23
27
|
</div>
|
24
28
|
</div>
|
29
|
+
<% if @host %>
|
30
|
+
<h3><%= _('Target: ') %><%= link_to(@host.name, host_path(@host)) %></h3>
|
25
31
|
|
26
|
-
<
|
27
|
-
|
28
|
-
|
29
|
-
<%= preview_box(@template_invocation, @host) %>
|
30
|
-
</div>
|
32
|
+
<div class="preview hidden">
|
33
|
+
<%= preview_box(@template_invocation, @host) %>
|
34
|
+
</div>
|
31
35
|
|
32
|
-
<div class="terminal" data-refresh-url="<%= template_invocation_path(@template_invocation) %>">
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
<div class="terminal" data-refresh-url="<%= template_invocation_path(@template_invocation) %>">
|
37
|
+
<% if @error %>
|
38
|
+
<div class="line error"><%= @error %></div>
|
39
|
+
<% else %>
|
40
|
+
<%= link_to_function(_('Scroll to bottom'), '$("html, body").animate({ scrollTop: $(document).height() }, "slow");', :class => 'pull-right scroll-link-bottom') %>
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
<div class="printable">
|
43
|
+
<%= render :partial => 'output_line_set', :collection => normalize_line_sets(@line_sets) %>
|
44
|
+
</div>
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
</div>
|
46
|
+
<%= link_to_function(_('Scroll to top'), '$("html, body").animate({ scrollTop: 0 }, "slow");', :class => 'pull-right scroll-link-top') %>
|
47
|
+
<% end %>
|
48
|
+
</div>
|
45
49
|
|
46
|
-
<script>
|
47
|
-
|
48
|
-
</script>
|
50
|
+
<script>
|
51
|
+
<%= render :partial => 'refresh.js' %>
|
52
|
+
</script>
|
53
|
+
<% else %>
|
54
|
+
<%= _("Could not display data for job invocation.") %>
|
55
|
+
<% end %>
|
@@ -98,6 +98,7 @@ handle_zypp_res_codes () {
|
|
98
98
|
end
|
99
99
|
-%>
|
100
100
|
[ -x "$(command -v subscription-manager)" ] && subscription-manager refresh
|
101
|
+
export DEBIAN_FRONTEND=noninteractive
|
101
102
|
apt-get -y update
|
102
103
|
apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y <%= action %> <%= input("package") %>
|
103
104
|
<% elsif package_manager == 'zypper' -%>
|
data/config/routes.rb
CHANGED
@@ -32,6 +32,15 @@ module ForemanRemoteExecution
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
# A workaround for https://projects.theforeman.org/issues/30685
|
36
|
+
initializer 'foreman_remote_execution.rails_loading_workaround' do
|
37
|
+
# Without this, in production environment the module gets prepended too
|
38
|
+
# late and the extensions do not get applied
|
39
|
+
# TODO: Remove this and from config.to_prepare once there is an extension
|
40
|
+
# point in Foreman
|
41
|
+
ProvisioningTemplatesHelper.prepend ForemanRemoteExecution::JobTemplatesExtensions
|
42
|
+
end
|
43
|
+
|
35
44
|
initializer 'foreman_remote_execution.apipie' do
|
36
45
|
Apipie.configuration.checksum_path += ['/api/']
|
37
46
|
end
|
@@ -74,7 +83,7 @@ module ForemanRemoteExecution
|
|
74
83
|
permission :create_job_invocations, { :job_invocations => [:new, :create, :refresh, :rerun, :preview_hosts],
|
75
84
|
'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
|
76
85
|
permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search], :template_invocations => [:show],
|
77
|
-
'api/v2/job_invocations' => [:index, :show, :output, :raw_output] }, :resource_type => 'JobInvocation'
|
86
|
+
'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs] }, :resource_type => 'JobInvocation'
|
78
87
|
permission :view_template_invocations, { :template_invocations => [:show],
|
79
88
|
'api/v2/template_invocations' => [:template_invocations] }, :resource_type => 'TemplateInvocation'
|
80
89
|
permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
|
@@ -138,7 +147,16 @@ module ForemanRemoteExecution
|
|
138
147
|
end
|
139
148
|
|
140
149
|
extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
|
150
|
+
extend_rabl_template 'api/v2/interfaces/main', 'api/v2/interfaces/execution_flag'
|
151
|
+
extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies'
|
152
|
+
parameter_filter ::Subnet, :remote_execution_proxy_ids
|
141
153
|
describe_host { overview_buttons_provider :host_overview_buttons }
|
154
|
+
|
155
|
+
# Extend Registration module
|
156
|
+
extend_allowed_registration_vars :remote_execution_interface
|
157
|
+
extend_page 'registration/_form' do |cx|
|
158
|
+
cx.add_pagelet :global_registration, name: N_('Remote Execution'), partial: 'api/v2/registration/form', priority: 100, id: 'remote_execution_interface'
|
159
|
+
end
|
142
160
|
end
|
143
161
|
end
|
144
162
|
|
@@ -184,6 +202,7 @@ module ForemanRemoteExecution
|
|
184
202
|
SmartProxy.prepend ForemanRemoteExecution::SmartProxyExtensions
|
185
203
|
Subnet.include ForemanRemoteExecution::SubnetExtensions
|
186
204
|
|
205
|
+
::Api::V2::InterfacesController.include Api::V2::InterfacesControllerExtensions
|
187
206
|
# We need to explicitly force to load the Task model due to Rails loader
|
188
207
|
# having issues with resolving it to Rake::Task otherwise
|
189
208
|
require_dependency 'foreman_tasks/task'
|
@@ -192,6 +211,10 @@ module ForemanRemoteExecution
|
|
192
211
|
RemoteExecutionProvider.register(:SSH, SSHExecutionProvider)
|
193
212
|
|
194
213
|
ForemanRemoteExecution.register_rex_feature
|
214
|
+
|
215
|
+
::Api::V2::SubnetsController.include ::ForemanRemoteExecution::Concerns::Api::V2::SubnetsControllerExtensions
|
216
|
+
::Api::V2::RegistrationController.prepend ::ForemanRemoteExecution::Concerns::Api::V2::RegistrationControllerExtensions
|
217
|
+
::Api::V2::RegistrationController.include ::ForemanRemoteExecution::Concerns::Api::V2::RegistrationControllerExtensions::ApipieExtensions
|
195
218
|
end
|
196
219
|
|
197
220
|
initializer 'foreman_remote_execution.register_gettext', after: :load_config_initializers do |_app|
|
data/package.json
CHANGED
@@ -21,16 +21,16 @@
|
|
21
21
|
},
|
22
22
|
"devDependencies": {
|
23
23
|
"@babel/core": "^7.7.0",
|
24
|
-
"@theforeman/builder": "^4.
|
25
|
-
"@theforeman/eslint-plugin-foreman": "^4.
|
26
|
-
"@theforeman/stories": "^4.
|
27
|
-
"@theforeman/test": "^4.
|
28
|
-
"@theforeman/vendor-dev": "^4.
|
24
|
+
"@theforeman/builder": "^4.14.0",
|
25
|
+
"@theforeman/eslint-plugin-foreman": "^4.14.0",
|
26
|
+
"@theforeman/stories": "^4.14.0",
|
27
|
+
"@theforeman/test": "^4.14.0",
|
28
|
+
"@theforeman/vendor-dev": "^4.14.0",
|
29
29
|
"babel-eslint": "^10.0.0",
|
30
30
|
"eslint": "^6.8.0",
|
31
31
|
"prettier": "^1.19.1"
|
32
32
|
},
|
33
33
|
"peerDependencies": {
|
34
|
-
"@theforeman/vendor": ">= 4.
|
34
|
+
"@theforeman/vendor": ">= 4.14.0"
|
35
35
|
}
|
36
36
|
}
|
@@ -160,6 +160,35 @@ module Api
|
|
160
160
|
end
|
161
161
|
end
|
162
162
|
|
163
|
+
describe '#outputs' do
|
164
|
+
test 'should provide outputs for hosts in the job' do
|
165
|
+
get :outputs, params: { :id => @invocation.id }
|
166
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
167
|
+
host_output = result['outputs'].first
|
168
|
+
assert_equal host_output['host_id'], @invocation.targeting.host_ids.first
|
169
|
+
assert_equal host_output['refresh'], true
|
170
|
+
assert_equal host_output['output'], []
|
171
|
+
assert_response :success
|
172
|
+
end
|
173
|
+
|
174
|
+
test 'should provide outputs for selected hosts in the job' do
|
175
|
+
post :outputs, params: { :id => @invocation.id, :search_query => "id = #{@invocation.targeting.host_ids.first}" }, as: :json
|
176
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
177
|
+
host_output = result['outputs'].first
|
178
|
+
assert_equal host_output['host_id'], @invocation.targeting.host_ids.first
|
179
|
+
assert_equal host_output['refresh'], true
|
180
|
+
assert_equal host_output['output'], []
|
181
|
+
assert_response :success
|
182
|
+
end
|
183
|
+
|
184
|
+
test 'should provide outputs for hosts in the job matching a search query' do
|
185
|
+
get :outputs, params: { :id => @invocation.id, :search_query => "name = definitely_not_in_the_job" }
|
186
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
187
|
+
assert_equal result['outputs'], []
|
188
|
+
assert_response :success
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
163
192
|
describe 'raw output' do
|
164
193
|
let(:fake_output) do
|
165
194
|
(1..5).map do |i|
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
module Api
|
4
|
+
module V2
|
5
|
+
# Tests for the extra methods to play roles on a Host
|
6
|
+
class RegistrationControllerTest < ActionController::TestCase
|
7
|
+
describe 'host registration' do
|
8
|
+
let(:organization) { FactoryBot.create(:organization) }
|
9
|
+
let(:tax_location) { FactoryBot.create(:location) }
|
10
|
+
let(:template_kind) { template_kinds(:registration) }
|
11
|
+
let(:registration_template) do
|
12
|
+
FactoryBot.create(
|
13
|
+
:provisioning_template,
|
14
|
+
template_kind: template_kind,
|
15
|
+
template: 'template content <%= @host.name %>',
|
16
|
+
locations: [tax_location],
|
17
|
+
organizations: [organization]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
let(:os) do
|
21
|
+
FactoryBot.create(
|
22
|
+
:operatingsystem,
|
23
|
+
:with_associations,
|
24
|
+
family: 'Redhat',
|
25
|
+
provisioning_templates: [
|
26
|
+
registration_template,
|
27
|
+
]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:host_params) do
|
32
|
+
{ host: { name: 'centos-test.example.com',
|
33
|
+
managed: false, build: false,
|
34
|
+
organization_id: organization.id,
|
35
|
+
location_id: tax_location.id,
|
36
|
+
operatingsystem_id: os.id } }
|
37
|
+
end
|
38
|
+
|
39
|
+
setup do
|
40
|
+
FactoryBot.create(
|
41
|
+
:os_default_template,
|
42
|
+
template_kind: template_kind,
|
43
|
+
provisioning_template: registration_template,
|
44
|
+
operatingsystem: os
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'remote_execution_interface' do
|
49
|
+
setup do
|
50
|
+
@host = Host.create(host_params[:host])
|
51
|
+
@interface0 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy0', execution: false)
|
52
|
+
end
|
53
|
+
|
54
|
+
test 'with existing interface' do
|
55
|
+
params = host_params.merge(remote_execution_interface: @interface0.identifier)
|
56
|
+
|
57
|
+
post :host, params: params, session: set_session_user
|
58
|
+
assert_response :success
|
59
|
+
assert @interface0.reload.execution
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'with not-existing interface' do
|
63
|
+
params = host_params.merge(remote_execution_interface: 'dummy999')
|
64
|
+
|
65
|
+
post :host, params: params, session: set_session_user
|
66
|
+
assert_response :not_found
|
67
|
+
end
|
68
|
+
|
69
|
+
test 'with multiple interfaces' do
|
70
|
+
interface1 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy1', execution: false)
|
71
|
+
params = host_params.merge(remote_execution_interface: interface1.identifier)
|
72
|
+
|
73
|
+
post :host, params: params, session: set_session_user
|
74
|
+
assert_response :success
|
75
|
+
refute @interface0.reload.execution
|
76
|
+
assert interface1.reload.execution
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -24,6 +24,7 @@ module ForemanRemoteExecution
|
|
24
24
|
OpenStruct.new(:id => uuid).tap do |o|
|
25
25
|
o.stubs(:add_missing_task_groups)
|
26
26
|
o.stubs(:task_groups).returns([])
|
27
|
+
o.stubs(:pending?).returns(true)
|
27
28
|
end
|
28
29
|
end
|
29
30
|
let(:action) do
|
@@ -73,7 +74,7 @@ module ForemanRemoteExecution
|
|
73
74
|
end
|
74
75
|
|
75
76
|
it 'triggers the RunHostJob actions on the resolved hosts in run phase' do
|
76
|
-
planned.expects(:output).returns(:planned_count => 0)
|
77
|
+
planned.expects(:output).at_most(5).returns(:planned_count => 0)
|
77
78
|
planned.expects(:trigger).with { |*args| args[0] == Actions::RemoteExecution::RunHostJob }
|
78
79
|
planned.create_sub_plans
|
79
80
|
end
|
@@ -51,8 +51,12 @@ const TargetingHosts = ({ apiStatus, items }) => {
|
|
51
51
|
};
|
52
52
|
|
53
53
|
TargetingHosts.propTypes = {
|
54
|
-
apiStatus: PropTypes.string
|
54
|
+
apiStatus: PropTypes.string,
|
55
55
|
items: PropTypes.array.isRequired,
|
56
56
|
};
|
57
57
|
|
58
|
+
TargetingHosts.defaultProps = {
|
59
|
+
apiStatus: null,
|
60
|
+
};
|
61
|
+
|
58
62
|
export default TargetingHosts;
|
@@ -52,11 +52,15 @@ const TargetingHostsPage = ({
|
|
52
52
|
TargetingHostsPage.propTypes = {
|
53
53
|
handleSearch: PropTypes.func.isRequired,
|
54
54
|
searchQuery: PropTypes.string.isRequired,
|
55
|
-
apiStatus: PropTypes.string
|
55
|
+
apiStatus: PropTypes.string,
|
56
56
|
items: PropTypes.array.isRequired,
|
57
57
|
totalHosts: PropTypes.number.isRequired,
|
58
58
|
pagination: PropTypes.object.isRequired,
|
59
59
|
handlePagination: PropTypes.func.isRequired,
|
60
60
|
};
|
61
61
|
|
62
|
+
TargetingHostsPage.defaultProps = {
|
63
|
+
apiStatus: null,
|
64
|
+
};
|
65
|
+
|
62
66
|
export default TargetingHostsPage;
|
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: 4.
|
4
|
+
version: 4.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -154,6 +154,8 @@ files:
|
|
154
154
|
- app/controllers/concerns/foreman/controller/parameters/job_template.rb
|
155
155
|
- app/controllers/concerns/foreman/controller/parameters/remote_execution_feature.rb
|
156
156
|
- app/controllers/concerns/foreman/controller/parameters/targeting.rb
|
157
|
+
- app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb
|
158
|
+
- app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb
|
157
159
|
- app/controllers/job_invocations_controller.rb
|
158
160
|
- app/controllers/job_templates_controller.rb
|
159
161
|
- app/controllers/remote_execution_features_controller.rb
|
@@ -170,6 +172,7 @@ files:
|
|
170
172
|
- app/lib/foreman_remote_execution/renderer/scope/input.rb
|
171
173
|
- app/lib/proxy_api/remote_execution_ssh.rb
|
172
174
|
- app/mailers/.gitkeep
|
175
|
+
- app/models/concerns/api/v2/interfaces_controller_extensions.rb
|
173
176
|
- app/models/concerns/foreman_remote_execution/bookmark_extensions.rb
|
174
177
|
- app/models/concerns/foreman_remote_execution/errors_flattener.rb
|
175
178
|
- app/models/concerns/foreman_remote_execution/foreman_tasks_cleaner_extensions.rb
|
@@ -212,6 +215,7 @@ files:
|
|
212
215
|
- app/views/api/v2/foreign_input_sets/index.json.rabl
|
213
216
|
- app/views/api/v2/foreign_input_sets/main.json.rabl
|
214
217
|
- app/views/api/v2/foreign_input_sets/show.json.rabl
|
218
|
+
- app/views/api/v2/interfaces/execution_flag.json.rabl
|
215
219
|
- app/views/api/v2/job_invocations/base.json.rabl
|
216
220
|
- app/views/api/v2/job_invocations/create.json.rabl
|
217
221
|
- app/views/api/v2/job_invocations/index.json.rabl
|
@@ -223,11 +227,13 @@ files:
|
|
223
227
|
- app/views/api/v2/job_templates/main.json.rabl
|
224
228
|
- app/views/api/v2/job_templates/show.json.rabl
|
225
229
|
- app/views/api/v2/job_templates/update.json.rabl
|
230
|
+
- app/views/api/v2/registration/_form.html.erb
|
226
231
|
- app/views/api/v2/remote_execution_features/base.json.rabl
|
227
232
|
- app/views/api/v2/remote_execution_features/index.json.rabl
|
228
233
|
- app/views/api/v2/remote_execution_features/main.json.rabl
|
229
234
|
- app/views/api/v2/remote_execution_features/show.json.rabl
|
230
235
|
- app/views/api/v2/smart_proxies/pubkey.json.rabl
|
236
|
+
- app/views/api/v2/subnets/remote_execution_proxies.json.rabl
|
231
237
|
- app/views/api/v2/template_invocations/base.json.rabl
|
232
238
|
- app/views/api/v2/template_invocations/template_invocations.json.rabl
|
233
239
|
- app/views/dashboard/.gitkeep
|
@@ -324,6 +330,7 @@ files:
|
|
324
330
|
- db/migrate/20180913101042_add_randomized_ordering_to_targeting.rb
|
325
331
|
- db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb
|
326
332
|
- db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb
|
333
|
+
- db/migrate/20200820122057_add_proxy_selector_override_to_remote_execution_feature.rb
|
327
334
|
- db/seeds.d/100-assign_features_with_templates.rb
|
328
335
|
- db/seeds.d/20-permissions.rb
|
329
336
|
- db/seeds.d/50-notification_blueprints.rb
|
@@ -372,6 +379,7 @@ files:
|
|
372
379
|
- test/functional/api/v2/foreign_input_sets_controller_test.rb
|
373
380
|
- test/functional/api/v2/job_invocations_controller_test.rb
|
374
381
|
- test/functional/api/v2/job_templates_controller_test.rb
|
382
|
+
- test/functional/api/v2/registration_controller_test.rb
|
375
383
|
- test/functional/api/v2/remote_execution_features_controller_test.rb
|
376
384
|
- test/functional/api/v2/template_invocations_controller_test.rb
|
377
385
|
- test/functional/cockpit_controller_test.rb
|
@@ -439,7 +447,7 @@ homepage: https://github.com/theforeman/foreman_remote_execution
|
|
439
447
|
licenses:
|
440
448
|
- GPL-3.0
|
441
449
|
metadata: {}
|
442
|
-
post_install_message:
|
450
|
+
post_install_message:
|
443
451
|
rdoc_options: []
|
444
452
|
require_paths:
|
445
453
|
- lib
|
@@ -454,8 +462,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
454
462
|
- !ruby/object:Gem::Version
|
455
463
|
version: '0'
|
456
464
|
requirements: []
|
457
|
-
rubygems_version: 3.
|
458
|
-
signing_key:
|
465
|
+
rubygems_version: 3.1.2
|
466
|
+
signing_key:
|
459
467
|
specification_version: 4
|
460
468
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
461
469
|
management functionality with remote management functionality.
|
@@ -466,6 +474,7 @@ test_files:
|
|
466
474
|
- test/functional/api/v2/foreign_input_sets_controller_test.rb
|
467
475
|
- test/functional/api/v2/job_invocations_controller_test.rb
|
468
476
|
- test/functional/api/v2/job_templates_controller_test.rb
|
477
|
+
- test/functional/api/v2/registration_controller_test.rb
|
469
478
|
- test/functional/api/v2/remote_execution_features_controller_test.rb
|
470
479
|
- test/functional/api/v2/template_invocations_controller_test.rb
|
471
480
|
- test/functional/cockpit_controller_test.rb
|