foreman_remote_execution 3.2.2 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc +5 -30
- data/.gitignore +1 -0
- data/.hound.yml +0 -5
- data/.travis.yml +2 -3
- data/app/controllers/api/v2/job_invocations_controller.rb +1 -5
- data/app/controllers/api/v2/template_invocations_controller.rb +1 -4
- data/app/helpers/job_invocations_helper.rb +1 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +3 -2
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +1 -1
- data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +5 -7
- data/app/models/remote_execution_provider.rb +5 -0
- data/app/services/default_proxy_proxy_selector.rb +18 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +2 -2
- data/db/seeds.d/70-job_templates.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +16 -33
- data/test/functional/api/v2/job_invocations_controller_test.rb +14 -42
- data/test/models/orchestration/ssh_test.rb +0 -32
- data/test/unit/concerns/host_extensions_test.rb +0 -7
- data/webpack/index.js +1 -2
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +10 -0
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +6 -3
- data/webpack/react_app/components/jobInvocations/index.js +19 -7
- data/webpack/react_app/redux/actions/jobInvocations/index.js +12 -8
- data/webpack/react_app/redux/consts.js +1 -2
- data/webpack/react_app/redux/reducers/jobInvocations/index.fixtures.js +8 -40
- data/webpack/react_app/redux/reducers/jobInvocations/index.test.js +17 -11
- data/webpack/test_setup.js +2 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d00f4722f74258ad947432c2d8104522d3fb53749f7bac4e7c52914862af3ecc
|
4
|
+
data.tar.gz: 187645d51578339523b94fb83fe781cba865477a616c2d8c0980255be1fd0377
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04c6a44e96bb03d75310d7dd48acd249503f04778c62f520d1101bf4719d195c2cd4d11e9a6ae63c97e24d8cbd50884dd090ec56a54aff933ee19f3379bbeec1
|
7
|
+
data.tar.gz: 165ba49b54b18ed4128c7b72a53ef52fea60f6f9a619ee7bd0a22545910cf68997b763e27f3569892b20dd528e70236c44b241c1d1b6c5fcec15a5a21f76af7e
|
data/.eslintrc
CHANGED
@@ -1,32 +1,7 @@
|
|
1
1
|
{
|
2
|
-
"
|
3
|
-
"extends": [
|
4
|
-
|
5
|
-
"
|
6
|
-
]
|
7
|
-
"env": {
|
8
|
-
"browser": true,
|
9
|
-
"es6": true,
|
10
|
-
"node": true,
|
11
|
-
"jasmine": true,
|
12
|
-
"jest": true
|
13
|
-
},
|
14
|
-
"parser": "babel-eslint",
|
15
|
-
"rules": {
|
16
|
-
"react/jsx-uses-vars": "error",
|
17
|
-
"react/jsx-uses-react": "error",
|
18
|
-
"no-unused-vars": [
|
19
|
-
"error",
|
20
|
-
{
|
21
|
-
"vars": "all",
|
22
|
-
"args": "none"
|
23
|
-
}
|
24
|
-
],
|
25
|
-
"no-underscore-dangle": "off",
|
26
|
-
"no-use-before-define": "off",
|
27
|
-
"import/prefer-default-export": "off",
|
28
|
-
// Import rules off for now due to HoundCI issue
|
29
|
-
"import/no-unresolved": "off",
|
30
|
-
"import/extensions": "off"
|
31
|
-
}
|
2
|
+
"plugins": ["@theforeman/foreman"],
|
3
|
+
"extends": [
|
4
|
+
"plugin:@theforeman/foreman/core",
|
5
|
+
"plugin:@theforeman/foreman/plugins"
|
6
|
+
]
|
32
7
|
}
|
data/.gitignore
CHANGED
data/.hound.yml
CHANGED
data/.travis.yml
CHANGED
@@ -19,10 +19,6 @@ module Api
|
|
19
19
|
api :GET, '/job_invocations/:id', N_('Show job invocation')
|
20
20
|
param :id, :identifier, :required => true
|
21
21
|
def show
|
22
|
-
@hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
23
|
-
@template_invocations = @job_invocation.template_invocations
|
24
|
-
.where(host: @hosts)
|
25
|
-
.includes(:input_values)
|
26
22
|
end
|
27
23
|
|
28
24
|
def_param_group :job_invocation do
|
@@ -150,7 +146,7 @@ module Api
|
|
150
146
|
end
|
151
147
|
|
152
148
|
def find_host
|
153
|
-
@host =
|
149
|
+
@host = Host.authorized(:view_hosts).find(params['host_id'])
|
154
150
|
rescue ActiveRecord::RecordNotFound
|
155
151
|
not_found({ :error => { :message => (_("Host with id '%{id}' was not found") % { :id => params['host_id'] }) } })
|
156
152
|
end
|
@@ -26,10 +26,7 @@ module Api
|
|
26
26
|
private
|
27
27
|
|
28
28
|
def resource_scope_for_template_invocations
|
29
|
-
@job_invocation.template_invocations
|
30
|
-
.includes(:host)
|
31
|
-
.where(host: Host.authorized(:view_hosts, Host))
|
32
|
-
.search_for(*search_options)
|
29
|
+
@job_invocation.template_invocations.search_for(*search_options)
|
33
30
|
end
|
34
31
|
|
35
32
|
def find_job_invocation
|
@@ -29,7 +29,7 @@ module JobInvocationsHelper
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def preview_hosts(template_invocation)
|
32
|
-
hosts = template_invocation.targeting.hosts.
|
32
|
+
hosts = template_invocation.targeting.hosts.take(20)
|
33
33
|
hosts.map do |host|
|
34
34
|
collapsed_preview(host) +
|
35
35
|
render(:partial => 'job_invocations/user_input',
|
@@ -29,6 +29,9 @@ module Actions
|
|
29
29
|
|
30
30
|
raise _('Could not use any template used in the job invocation') if template_invocation.blank?
|
31
31
|
|
32
|
+
provider = template_invocation.template.provider
|
33
|
+
proxy_selector = provider.required_proxy_selector_for(template_invocation.template) || proxy_selector
|
34
|
+
|
32
35
|
provider_type = template_invocation.template.provider_type.to_s
|
33
36
|
proxy = determine_proxy!(proxy_selector, provider_type, host)
|
34
37
|
|
@@ -36,8 +39,6 @@ module Actions
|
|
36
39
|
script = renderer.render
|
37
40
|
raise _('Failed rendering template: %s') % renderer.error_message unless script
|
38
41
|
|
39
|
-
provider = template_invocation.template.provider
|
40
|
-
|
41
42
|
additional_options = { :hostname => provider.find_ip_or_hostname(host),
|
42
43
|
:script => script,
|
43
44
|
:execution_timeout_interval => job_invocation.execution_timeout_interval,
|
@@ -49,7 +49,7 @@ module ForemanRemoteExecution
|
|
49
49
|
keys = remote_execution_ssh_keys
|
50
50
|
source = 'global'
|
51
51
|
if keys.present?
|
52
|
-
value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [
|
52
|
+
value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| v || [] }
|
53
53
|
params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source}
|
54
54
|
end
|
55
55
|
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
@@ -8,10 +8,8 @@ module ForemanRemoteExecution
|
|
8
8
|
register_rebuild(:queue_ssh_destroy, N_("SSH_#{self.to_s.split('::').first}"))
|
9
9
|
end
|
10
10
|
|
11
|
-
def drop_from_known_hosts(
|
12
|
-
|
13
|
-
return true if target.nil?
|
14
|
-
|
11
|
+
def drop_from_known_hosts(args)
|
12
|
+
proxy_id, target = args
|
15
13
|
proxy = ::SmartProxy.find(proxy_id)
|
16
14
|
begin
|
17
15
|
proxy.drop_host_from_known_hosts(target)
|
@@ -28,11 +26,11 @@ module ForemanRemoteExecution
|
|
28
26
|
def ssh_destroy
|
29
27
|
logger.debug "Scheduling SSH known_hosts cleanup"
|
30
28
|
|
31
|
-
host, _kind,
|
29
|
+
host, _kind, target = host_kind_target
|
32
30
|
proxies = host.remote_execution_proxies('SSH').values
|
33
31
|
proxies.flatten.uniq.each do |proxy|
|
34
32
|
queue.create(id: queue_id(proxy.id), name: _("Remove SSH known hosts for %s") % self,
|
35
|
-
priority: 200, action: [self, :drop_from_known_hosts, proxy.id])
|
33
|
+
priority: 200, action: [self, :drop_from_known_hosts, [proxy.id, target]])
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
@@ -42,7 +40,7 @@ module ForemanRemoteExecution
|
|
42
40
|
|
43
41
|
def should_drop_from_known_hosts?
|
44
42
|
host, = host_kind_target
|
45
|
-
host
|
43
|
+
host&.build && host&.changes&.key?('build')
|
46
44
|
end
|
47
45
|
|
48
46
|
private
|
@@ -98,5 +98,10 @@ class RemoteExecutionProvider
|
|
98
98
|
def proxy_action_class
|
99
99
|
ForemanRemoteExecutionCore::Actions::RunScript
|
100
100
|
end
|
101
|
+
|
102
|
+
# Return a specific proxy selector to use for running a given template
|
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(_template)
|
105
|
+
end
|
101
106
|
end
|
102
107
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class DefaultProxyProxySelector < ::RemoteExecutionProxySelector
|
2
|
+
def initialize
|
3
|
+
# TODO: Remove this once we have a reliable way of determining the internal proxy without katello
|
4
|
+
# Tracked as https://projects.theforeman.org/issues/29840
|
5
|
+
raise _('Internal proxy selector can only be used if Katello is enabled') unless defined?(::Katello)
|
6
|
+
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def available_proxies(host, provider)
|
11
|
+
# TODO: Once we have a internal proxy marker/feature on the proxy, we can
|
12
|
+
# swap the implementation
|
13
|
+
internal_proxy = ::Katello.default_capsule
|
14
|
+
super.reduce({}) do |acc, (key, proxies)|
|
15
|
+
acc.merge(key => proxies.select { |proxy| proxy == internal_proxy })
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -19,7 +19,7 @@ child :targeting do
|
|
19
19
|
attributes :bookmark_id, :search_query, :targeting_type, :user_id, :status, :status_label,
|
20
20
|
:randomized_ordering
|
21
21
|
|
22
|
-
child
|
22
|
+
child :hosts do
|
23
23
|
extends 'api/v2/hosts/base'
|
24
24
|
end
|
25
25
|
end
|
@@ -28,7 +28,7 @@ child :task do
|
|
28
28
|
attributes :id, :state
|
29
29
|
end
|
30
30
|
|
31
|
-
child
|
31
|
+
child :template_invocations do
|
32
32
|
attributes :template_id, :template_name
|
33
33
|
child :input_values do
|
34
34
|
attributes :template_input_name, :template_input_id
|
@@ -4,7 +4,7 @@ User.as_anonymous_admin do
|
|
4
4
|
JobTemplate.without_auditing do
|
5
5
|
Dir[File.join("#{ForemanRemoteExecution::Engine.root}/app/views/templates/**/*.erb")].each do |template|
|
6
6
|
sync = !Rails.env.test? && Setting[:remote_execution_sync_templates]
|
7
|
-
template = JobTemplate.import_raw!(File.read(template), :default => true, :
|
7
|
+
template = JobTemplate.import_raw!(File.read(template), :default => true, :lock => true, :update => sync)
|
8
8
|
template.organizations = organizations if template.present?
|
9
9
|
template.locations = locations if template.present?
|
10
10
|
end
|
data/package.json
CHANGED
@@ -3,25 +3,14 @@
|
|
3
3
|
"version": "1.0.0",
|
4
4
|
"license": "GPL-3.0",
|
5
5
|
"scripts": {
|
6
|
-
"lint": "
|
7
|
-
"test": "
|
8
|
-
"test:watch": "
|
9
|
-
"test:current": "
|
10
|
-
|
11
|
-
|
12
|
-
"
|
13
|
-
"
|
14
|
-
"node_modules",
|
15
|
-
"webpack"
|
16
|
-
],
|
17
|
-
"setupFiles": [
|
18
|
-
"raf/polyfill",
|
19
|
-
"./webpack/test_setup.js"
|
20
|
-
],
|
21
|
-
"testPathIgnorePatterns": [
|
22
|
-
"/node_modules/",
|
23
|
-
"<rootDir>/foreman/"
|
24
|
-
]
|
6
|
+
"lint": "tfm-lint --plugin -d /webpack",
|
7
|
+
"test": "tfm-test --plugin",
|
8
|
+
"test:watch": "tfm-test --plugin --watchAll",
|
9
|
+
"test:current": "tfm-test --plugin --watch",
|
10
|
+
"publish-coverage": "tfm-publish-coverage",
|
11
|
+
"stories": "tfm-stories --plugin",
|
12
|
+
"stories:build": "tfm-build-stories --plugin",
|
13
|
+
"stories:deploy": "surge --project .storybook-dist"
|
25
14
|
},
|
26
15
|
"repository": {
|
27
16
|
"type": "git",
|
@@ -32,22 +21,16 @@
|
|
32
21
|
},
|
33
22
|
"devDependencies": {
|
34
23
|
"@babel/core": "^7.7.0",
|
35
|
-
"@theforeman/builder": "^4.
|
36
|
-
"@theforeman/
|
24
|
+
"@theforeman/builder": "^4.2.1",
|
25
|
+
"@theforeman/eslint-plugin-foreman": "^4.2.1",
|
26
|
+
"@theforeman/stories": "^4.2.1",
|
27
|
+
"@theforeman/test": "^4.2.1",
|
28
|
+
"@theforeman/vendor-dev": "^4.2.1",
|
37
29
|
"babel-eslint": "^10.0.0",
|
38
|
-
"
|
39
|
-
"
|
40
|
-
"enzyme-adapter-react-16": "^1.1.0",
|
41
|
-
"enzyme-to-json": "^3.1.2",
|
42
|
-
"eslint": "^4.10.0",
|
43
|
-
"eslint-config-airbnb": "^16.0.0",
|
44
|
-
"eslint-plugin-import": "^2.8.0",
|
45
|
-
"eslint-plugin-jest": "^21.2.0",
|
46
|
-
"eslint-plugin-jsx-a11y": "^6.0.2",
|
47
|
-
"eslint-plugin-react": "^7.4.0",
|
48
|
-
"jest": "^24.9.0"
|
30
|
+
"eslint": "^6.8.0",
|
31
|
+
"prettier": "^1.19.1"
|
49
32
|
},
|
50
33
|
"peerDependencies": {
|
51
|
-
"@theforeman/vendor": ">= 4.
|
34
|
+
"@theforeman/vendor": ">= 4.2.1"
|
52
35
|
}
|
53
36
|
}
|
@@ -4,7 +4,7 @@ module Api
|
|
4
4
|
module V2
|
5
5
|
class JobInvocationsControllerTest < ActionController::TestCase
|
6
6
|
setup do
|
7
|
-
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task
|
7
|
+
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
8
8
|
@template = FactoryBot.create(:job_template, :with_input)
|
9
9
|
|
10
10
|
# Without this the template in template_invocations and in pattern_template_invocations
|
@@ -20,32 +20,18 @@ module Api
|
|
20
20
|
assert_response :success
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
assert_not_empty template['targeting']['hosts']
|
31
|
-
end
|
32
|
-
|
33
|
-
test 'should get invocation detail when taxonomies are set' do
|
34
|
-
taxonomy_params = %w(organization location).reduce({}) { |acc, cur| acc.merge("#{cur}_id" => FactoryBot.create(cur)) }
|
35
|
-
get :show, params: taxonomy_params.merge(:id => @invocation.id)
|
36
|
-
assert_response :success
|
37
|
-
end
|
38
|
-
|
39
|
-
test 'should see only permitted hosts' do
|
40
|
-
@user = FactoryBot.create(:user, admin: false)
|
41
|
-
setup_user('view', 'job_invocations', nil, @user)
|
42
|
-
setup_user('view', 'hosts', 'name ~ nope.example.com', @user)
|
23
|
+
test 'should get invocation detail' do
|
24
|
+
get :show, params: { :id => @invocation.id }
|
25
|
+
assert_response :success
|
26
|
+
template = ActiveSupport::JSON.decode(@response.body)
|
27
|
+
assert_not_empty template
|
28
|
+
assert_equal template['job_category'], @invocation.job_category
|
29
|
+
end
|
43
30
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
31
|
+
test 'should get invocation detail when taxonomies are set' do
|
32
|
+
taxonomy_params = %w(organization location).reduce({}) { |acc, cur| acc.merge("#{cur}_id" => FactoryBot.create(cur)) }
|
33
|
+
get :show, params: taxonomy_params.merge(:id => @invocation.id)
|
34
|
+
assert_response :success
|
49
35
|
end
|
50
36
|
|
51
37
|
context 'creation' do
|
@@ -122,7 +108,7 @@ module Api
|
|
122
108
|
end
|
123
109
|
|
124
110
|
describe '#output' do
|
125
|
-
let(:host) { @invocation.
|
111
|
+
let(:host) { @invocation.template_invocations_hosts.first }
|
126
112
|
|
127
113
|
test 'should provide output for delayed task' do
|
128
114
|
ForemanTasks::Task.any_instance.expects(:scheduled?).returns(true)
|
@@ -151,12 +137,6 @@ module Api
|
|
151
137
|
assert_equal result['message'], "Job invocation not found by id '#{invocation_id}'"
|
152
138
|
assert_response :missing
|
153
139
|
end
|
154
|
-
|
155
|
-
test 'should get output only for host in job invocation' do
|
156
|
-
get :output, params: { job_invocation_id: @invocation.id,
|
157
|
-
host_id: FactoryBot.create(:host).id }
|
158
|
-
assert_response :missing
|
159
|
-
end
|
160
140
|
end
|
161
141
|
|
162
142
|
describe 'raw output' do
|
@@ -168,7 +148,7 @@ module Api
|
|
168
148
|
let(:fake_task) do
|
169
149
|
OpenStruct.new :pending? => false, :main_action => OpenStruct.new(:live_output => fake_output)
|
170
150
|
end
|
171
|
-
let(:host) { @invocation.
|
151
|
+
let(:host) { @invocation.template_invocations_hosts.first }
|
172
152
|
|
173
153
|
test 'should provide raw output for a host' do
|
174
154
|
JobInvocation.any_instance.expects(:task).returns(OpenStruct.new(:scheduled? => false))
|
@@ -204,12 +184,6 @@ module Api
|
|
204
184
|
assert_nil result['output']
|
205
185
|
assert_response :success
|
206
186
|
end
|
207
|
-
|
208
|
-
test 'should get raw output only for host in job invocation' do
|
209
|
-
get :raw_output, params: { job_invocation_id: @invocation.id,
|
210
|
-
host_id: FactoryBot.create(:host).id }
|
211
|
-
assert_response :missing
|
212
|
-
end
|
213
187
|
end
|
214
188
|
|
215
189
|
test 'should cancel a job' do
|
@@ -258,13 +232,11 @@ module Api
|
|
258
232
|
end
|
259
233
|
|
260
234
|
test 'should not raise an exception when reruning failed has no hosts' do
|
261
|
-
@invocation.targeting.hosts.first.destroy
|
262
235
|
JobInvocation.any_instance.expects(:generate_description)
|
263
236
|
JobInvocationComposer.any_instance
|
264
237
|
.expects(:validate_job_category)
|
265
238
|
.with(@invocation.job_category)
|
266
239
|
.returns(@invocation.job_category)
|
267
|
-
|
268
240
|
post :rerun, params: { :id => @invocation.id, :failed_only => true }
|
269
241
|
assert_response :success
|
270
242
|
result = ActiveSupport::JSON.decode(@response.body)
|
@@ -15,42 +15,10 @@ class SSHOrchestrationTest < ActiveSupport::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
it 'attempts to drop IP address and hostname from smart proxies on rebuild' do
|
18
|
-
host.stubs(:skip_orchestration?).returns false
|
19
|
-
SmartProxy.any_instance.expects(:drop_host_from_known_hosts).with(interface.ip)
|
20
|
-
SmartProxy.any_instance.expects(:drop_host_from_known_hosts).with(host.name)
|
21
|
-
|
22
18
|
host.build = true
|
23
19
|
host.save!
|
24
|
-
|
25
20
|
ids = ["ssh_remove_known_hosts_interface_#{interface.ip}_#{proxy.id}",
|
26
21
|
"ssh_remove_known_hosts_host_#{host.name}_#{proxy.id}"]
|
27
22
|
_(host.queue.task_ids).must_equal ids
|
28
|
-
_(host.queue.items.map(&:status)).must_equal %w(completed completed)
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'does not fail on 404 from the smart proxy' do
|
32
|
-
host.stubs(:skip_orchestration?).returns false
|
33
|
-
SmartProxy.any_instance.expects(:drop_host_from_known_hosts).raises(RestClient::ResourceNotFound).twice
|
34
|
-
host.build = true
|
35
|
-
host.save!
|
36
|
-
ids = ["ssh_remove_known_hosts_interface_#{interface.ip}_#{proxy.id}",
|
37
|
-
"ssh_remove_known_hosts_host_#{host.name}_#{proxy.id}"]
|
38
|
-
_(host.queue.task_ids).must_equal ids
|
39
|
-
_(host.queue.items.map(&:status)).must_equal %w(completed completed)
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'does not trigger the removal when creating a new host' do
|
43
|
-
SmartProxy.any_instance.expects(:drop_host_from_known_hosts).never
|
44
|
-
host = Host::Managed.new(:name => 'test', :ip => '127.0.0.1')
|
45
|
-
host.stubs(:skip_orchestration?).returns false
|
46
|
-
_(host.queue.task_ids).must_equal []
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'does not call to the proxy when target is nil' do
|
50
|
-
host.stubs(:skip_orchestration?).returns false
|
51
|
-
SmartProxy.any_instance.expects(:drop_host_from_known_hosts).with(host.name)
|
52
|
-
host.interfaces.first.stubs(:ip)
|
53
|
-
host.destroy
|
54
|
-
_(host.queue.items.map(&:status)).must_equal %w(completed completed)
|
55
23
|
end
|
56
24
|
end
|
@@ -48,13 +48,6 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
48
48
|
_(host.host_param('remote_execution_ssh_keys')).must_include sshkey
|
49
49
|
end
|
50
50
|
|
51
|
-
it 'merges ssh key as a string from host parameters and proxies' do
|
52
|
-
key = 'ssh-rsa not-even-a-key something@somewhere.com'
|
53
|
-
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_keys', :value => key)
|
54
|
-
_(host.host_param('remote_execution_ssh_keys')).must_include key
|
55
|
-
_(host.host_param('remote_execution_ssh_keys')).must_include sshkey
|
56
|
-
end
|
57
|
-
|
58
51
|
it 'has ssh keys in the parameters even when no user specified' do
|
59
52
|
# this is a case, when using the helper in provisioning templates
|
60
53
|
FactoryBot.create(:smart_proxy, :ssh)
|
data/webpack/index.js
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
import URI from 'urijs';
|
2
1
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
3
|
-
import {
|
2
|
+
import { registerReducer } from 'foremanReact/common/MountingService';
|
4
3
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
5
4
|
import componentRegistry from 'foremanReact/components/componentRegistry';
|
6
5
|
import JobInvocationContainer from './react_app/components/jobInvocations';
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
2
3
|
|
3
4
|
const AggregateStatus = ({ statuses }) => (
|
4
5
|
<div id="aggregate_statuses">
|
@@ -31,4 +32,13 @@ const AggregateStatus = ({ statuses }) => (
|
|
31
32
|
</div>
|
32
33
|
);
|
33
34
|
|
35
|
+
AggregateStatus.propTypes = {
|
36
|
+
statuses: PropTypes.shape({
|
37
|
+
cancelled: PropTypes.number,
|
38
|
+
failed: PropTypes.number,
|
39
|
+
pending: PropTypes.number,
|
40
|
+
success: PropTypes.number,
|
41
|
+
}).isRequired,
|
42
|
+
};
|
43
|
+
|
34
44
|
export default AggregateStatus;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import { shallow } from '
|
3
|
-
import AggregateStatus from './index
|
2
|
+
import { shallow } from '@theforeman/test';
|
3
|
+
import AggregateStatus from './index';
|
4
4
|
|
5
5
|
jest.unmock('./index.js');
|
6
6
|
|
@@ -20,7 +20,10 @@ describe('AggregateStatus', () => {
|
|
20
20
|
|
21
21
|
it('renders cards with props passed', () => {
|
22
22
|
const statuses = {
|
23
|
-
success: 19,
|
23
|
+
success: 19,
|
24
|
+
failed: 20,
|
25
|
+
cancelled: 31,
|
26
|
+
pending: 3,
|
24
27
|
};
|
25
28
|
const chartNumbers = shallow(<AggregateStatus statuses={statuses} />);
|
26
29
|
const success = chartNumbers.find('#success_count').text();
|
@@ -4,14 +4,18 @@ import Immutable from 'seamless-immutable';
|
|
4
4
|
import PropTypes from 'prop-types';
|
5
5
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
6
6
|
import DonutChart from 'foremanReact/components/common/charts/DonutChart';
|
7
|
-
import AggregateStatus from './AggregateStatus
|
7
|
+
import AggregateStatus from './AggregateStatus';
|
8
8
|
import * as JobInvocationActions from '../../redux/actions/jobInvocations';
|
9
9
|
|
10
|
-
const colIndexOfMaxValue = columns =>
|
10
|
+
const colIndexOfMaxValue = columns =>
|
11
|
+
columns.reduce((iMax, x, i, arr) => (x[1] > arr[iMax][1] ? i : iMax), 0);
|
11
12
|
|
12
13
|
class JobInvocationContainer extends React.Component {
|
13
14
|
componentDidMount() {
|
14
|
-
const {
|
15
|
+
const {
|
16
|
+
startJobInvocationsPolling,
|
17
|
+
data: { url },
|
18
|
+
} = this.props;
|
15
19
|
|
16
20
|
startJobInvocationsPolling(url);
|
17
21
|
}
|
@@ -22,15 +26,20 @@ class JobInvocationContainer extends React.Component {
|
|
22
26
|
|
23
27
|
return (
|
24
28
|
<div id="job_invocations_chart_container">
|
25
|
-
<DonutChart
|
26
|
-
|
29
|
+
<DonutChart
|
30
|
+
data={Immutable.asMutable(jobInvocations)}
|
31
|
+
title={{
|
32
|
+
type: 'percent',
|
33
|
+
secondary: (jobInvocations[iMax] || [])[0],
|
34
|
+
}}
|
35
|
+
/>
|
27
36
|
<AggregateStatus statuses={statuses} />
|
28
37
|
</div>
|
29
38
|
);
|
30
39
|
}
|
31
40
|
}
|
32
41
|
|
33
|
-
const mapStateToProps =
|
42
|
+
const mapStateToProps = state => {
|
34
43
|
const {
|
35
44
|
jobInvocations,
|
36
45
|
statuses,
|
@@ -62,4 +71,7 @@ JobInvocationContainer.defaultProps = {
|
|
62
71
|
jobInvocations: [['property', 3, 'color']],
|
63
72
|
statuses: {},
|
64
73
|
};
|
65
|
-
export default connect(
|
74
|
+
export default connect(
|
75
|
+
mapStateToProps,
|
76
|
+
JobInvocationActions
|
77
|
+
)(JobInvocationContainer);
|
@@ -8,10 +8,10 @@ import {
|
|
8
8
|
} from '../../consts';
|
9
9
|
|
10
10
|
const defaultJobInvocationsPollingInterval = 1000;
|
11
|
-
const jobInvocationsInterval =
|
12
|
-
defaultJobInvocationsPollingInterval;
|
11
|
+
const jobInvocationsInterval =
|
12
|
+
process.env.JOB_INVOCATIONS_POLLING || defaultJobInvocationsPollingInterval;
|
13
13
|
|
14
|
-
const getJobInvocations = url => (dispatch, getState) => {
|
14
|
+
const getJobInvocations = url => async (dispatch, getState) => {
|
15
15
|
function onGetJobInvocationsSuccess({ data }) {
|
16
16
|
// If the job has finished, stop polling
|
17
17
|
if (data.finished) {
|
@@ -41,7 +41,7 @@ const getJobInvocations = url => (dispatch, getState) => {
|
|
41
41
|
if (jobInvocationsInterval) {
|
42
42
|
setTimeout(
|
43
43
|
() => dispatch(getJobInvocations(url)),
|
44
|
-
jobInvocationsInterval
|
44
|
+
jobInvocationsInterval
|
45
45
|
);
|
46
46
|
}
|
47
47
|
}
|
@@ -52,10 +52,14 @@ const getJobInvocations = url => (dispatch, getState) => {
|
|
52
52
|
|
53
53
|
if (getState().foremanRemoteExecutionReducers.jobInvocations.isPolling) {
|
54
54
|
if (isDocumentVisible) {
|
55
|
-
|
56
|
-
.
|
57
|
-
|
58
|
-
|
55
|
+
try {
|
56
|
+
const data = await API.get(url);
|
57
|
+
onGetJobInvocationsSuccess(data);
|
58
|
+
} catch (error) {
|
59
|
+
onGetJobInvocationsFailed(error);
|
60
|
+
} finally {
|
61
|
+
triggerPolling();
|
62
|
+
}
|
59
63
|
} else {
|
60
64
|
// document is not visible, keep polling without api call
|
61
65
|
triggerPolling();
|
@@ -2,5 +2,4 @@ export const JOB_INVOCATIONS_POLLING_STARTED =
|
|
2
2
|
'JOB_INVOCATIONS_POLLING_STARTED';
|
3
3
|
export const JOB_INVOCATIONS_GET_JOB_INVOCATIONS =
|
4
4
|
'JOB_INVOCATIONS_GET_JOB_INVOCATIONS';
|
5
|
-
export const JOB_INVOCATIONS_JOB_FINISHED =
|
6
|
-
'JOB_INVOCATIONS_JOB_FINISHED';
|
5
|
+
export const JOB_INVOCATIONS_JOB_FINISHED = 'JOB_INVOCATIONS_JOB_FINISHED';
|
@@ -15,26 +15,10 @@ export const pollingStarted = Immutable({
|
|
15
15
|
export const jobInvocationsPayload = Immutable({
|
16
16
|
jobInvocations: {
|
17
17
|
job_invocations: [
|
18
|
-
[
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
],
|
23
|
-
[
|
24
|
-
'Failed',
|
25
|
-
20,
|
26
|
-
'#B7312D',
|
27
|
-
],
|
28
|
-
[
|
29
|
-
'Pending',
|
30
|
-
40,
|
31
|
-
'#B7312D',
|
32
|
-
],
|
33
|
-
[
|
34
|
-
'Cancelled',
|
35
|
-
0,
|
36
|
-
'#B7312D',
|
37
|
-
],
|
18
|
+
['Success', 100, '#B7312D'],
|
19
|
+
['Failed', 20, '#B7312D'],
|
20
|
+
['Pending', 40, '#B7312D'],
|
21
|
+
['Cancelled', 0, '#B7312D'],
|
38
22
|
],
|
39
23
|
statuses: {
|
40
24
|
cancelled: 0,
|
@@ -48,26 +32,10 @@ export const jobInvocationsPayload = Immutable({
|
|
48
32
|
export const jobInvocationsReceived = Immutable({
|
49
33
|
isPolling: true,
|
50
34
|
jobInvocations: [
|
51
|
-
[
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
],
|
56
|
-
[
|
57
|
-
'Failed',
|
58
|
-
20,
|
59
|
-
'#B7312D',
|
60
|
-
],
|
61
|
-
[
|
62
|
-
'Pending',
|
63
|
-
40,
|
64
|
-
'#B7312D',
|
65
|
-
],
|
66
|
-
[
|
67
|
-
'Cancelled',
|
68
|
-
0,
|
69
|
-
'#B7312D',
|
70
|
-
],
|
35
|
+
['Success', 100, '#B7312D'],
|
36
|
+
['Failed', 20, '#B7312D'],
|
37
|
+
['Pending', 40, '#B7312D'],
|
38
|
+
['Cancelled', 0, '#B7312D'],
|
71
39
|
],
|
72
40
|
statuses: {
|
73
41
|
cancelled: 0,
|
@@ -18,20 +18,26 @@ describe('job invocations chart reducer', () => {
|
|
18
18
|
expect(reducer(undefined, {})).toEqual(initialState);
|
19
19
|
});
|
20
20
|
it('should start polling given POLLING_STARTED', () => {
|
21
|
-
expect(
|
22
|
-
|
23
|
-
|
21
|
+
expect(
|
22
|
+
reducer(initialState, {
|
23
|
+
type: JOB_INVOCATIONS_POLLING_STARTED,
|
24
|
+
})
|
25
|
+
).toEqual(pollingStarted);
|
24
26
|
});
|
25
27
|
it('should stop polling given JOB_FINISHED', () => {
|
26
|
-
expect(
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
expect(
|
29
|
+
reducer(pollingStarted, {
|
30
|
+
type: JOB_INVOCATIONS_JOB_FINISHED,
|
31
|
+
payload: { jobInvocations: { job_invocations: [], statuses: {} } },
|
32
|
+
})
|
33
|
+
).toEqual(initialState);
|
30
34
|
});
|
31
35
|
it('should receive job invocations given GET_JOB_INVOCATIONS', () => {
|
32
|
-
expect(
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
expect(
|
37
|
+
reducer(pollingStarted, {
|
38
|
+
type: JOB_INVOCATIONS_GET_JOB_INVOCATIONS,
|
39
|
+
payload: jobInvocationsPayload,
|
40
|
+
})
|
41
|
+
).toEqual(jobInvocationsReceived);
|
36
42
|
});
|
37
43
|
});
|
data/webpack/test_setup.js
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import 'core-js/shim';
|
2
2
|
import 'regenerator-runtime/runtime';
|
3
3
|
|
4
|
-
import { configure } from '
|
4
|
+
import { configure } from '@theforeman/test';
|
5
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
5
6
|
import Adapter from 'enzyme-adapter-react-16';
|
6
7
|
|
7
8
|
configure({ adapter: new Adapter() });
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -205,6 +205,7 @@ files:
|
|
205
205
|
- app/models/template_invocation_input_value.rb
|
206
206
|
- app/overrides/execution_interface.rb
|
207
207
|
- app/overrides/subnet_proxies.rb
|
208
|
+
- app/services/default_proxy_proxy_selector.rb
|
208
209
|
- app/services/remote_execution_proxy_selector.rb
|
209
210
|
- app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb
|
210
211
|
- app/views/api/v2/foreign_input_sets/base.json.rabl
|
@@ -426,7 +427,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
426
427
|
- !ruby/object:Gem::Version
|
427
428
|
version: '0'
|
428
429
|
requirements: []
|
429
|
-
rubygems_version: 3.0.
|
430
|
+
rubygems_version: 3.0.4
|
430
431
|
signing_key:
|
431
432
|
specification_version: 4
|
432
433
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|