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