foreman_remote_execution 4.2.3 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/js_ci.yml +29 -0
- data/.github/workflows/{ci.yml → ruby_ci.yml} +22 -50
- data/.prettierrc +4 -0
- data/.rubocop.yml +13 -49
- data/.rubocop_todo.yml +326 -102
- data/Gemfile +1 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +18 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +38 -1
- data/app/models/host_status/execution_status.rb +7 -0
- data/app/models/job_invocation.rb +1 -1
- data/config/routes.rb +3 -0
- data/foreman_remote_execution.gemspec +1 -2
- data/lib/foreman_remote_execution/engine.rb +12 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +15 -5
- data/test/functional/api/v2/registration_controller_test.rb +4 -13
- data/test/functional/ui_job_wizard_controller_test.rb +16 -0
- data/webpack/JobWizard/JobWizard.js +32 -0
- data/webpack/JobWizard/index.js +32 -0
- data/webpack/Routes/routes.js +12 -0
- data/webpack/__mocks__/foremanReact/history.js +1 -0
- data/webpack/global_index.js +4 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -1
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
- metadata +15 -19
data/Gemfile
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
class UiJobWizardController < ::Api::V2::BaseController
|
2
|
+
def categories
|
3
|
+
job_categories = resource_scope
|
4
|
+
.search_for("job_category ~ \"#{params[:search]}\"")
|
5
|
+
.select(:job_category).distinct
|
6
|
+
.reorder(:job_category)
|
7
|
+
.pluck(:job_category)
|
8
|
+
render :json => {:job_categories =>job_categories}
|
9
|
+
end
|
10
|
+
|
11
|
+
def resource_class
|
12
|
+
JobTemplate
|
13
|
+
end
|
14
|
+
|
15
|
+
def action_permission
|
16
|
+
:view_job_templates
|
17
|
+
end
|
18
|
+
end
|
@@ -3,6 +3,7 @@ module Actions
|
|
3
3
|
class RunHostJob < Actions::EntryAction
|
4
4
|
include ::Actions::Helpers::WithContinuousOutput
|
5
5
|
include ::Actions::Helpers::WithDelegatedAction
|
6
|
+
include ::Actions::ObservableAction
|
6
7
|
|
7
8
|
middleware.do_not_use Dynflow::Middleware::Common::Transaction
|
8
9
|
middleware.use Actions::Middleware::HideSecrets
|
@@ -16,7 +17,7 @@ module Actions
|
|
16
17
|
end
|
17
18
|
|
18
19
|
def plan(job_invocation, host, template_invocation, proxy_selector = ::RemoteExecutionProxySelector.new, options = {})
|
19
|
-
action_subject(host, :job_category => job_invocation.job_category, :description => job_invocation.description)
|
20
|
+
action_subject(host, :job_category => job_invocation.job_category, :description => job_invocation.description, :job_invocation_id => job_invocation.id)
|
20
21
|
|
21
22
|
template_invocation.host_id = host.id
|
22
23
|
template_invocation.run_host_job_task_id = task.id
|
@@ -121,6 +122,26 @@ module Actions
|
|
121
122
|
delegated_output[:exit_status]
|
122
123
|
end
|
123
124
|
|
125
|
+
def host_id
|
126
|
+
input['host']['id']
|
127
|
+
end
|
128
|
+
|
129
|
+
def host_name
|
130
|
+
input['host']['name']
|
131
|
+
end
|
132
|
+
|
133
|
+
def job_invocation_id
|
134
|
+
input['job_invocation_id']
|
135
|
+
end
|
136
|
+
|
137
|
+
def job_invocation
|
138
|
+
@job_invocation ||= ::JobInvocation.authorized.find(job_invocation_id)
|
139
|
+
end
|
140
|
+
|
141
|
+
def host
|
142
|
+
@host ||= ::Host.authorized.find(host_id)
|
143
|
+
end
|
144
|
+
|
124
145
|
private
|
125
146
|
|
126
147
|
def update_host_status
|
@@ -173,6 +194,22 @@ module Actions
|
|
173
194
|
end
|
174
195
|
proxy
|
175
196
|
end
|
197
|
+
|
198
|
+
extend ApipieDSL::Class
|
199
|
+
apipie :class, "An action representing execution of a job against a host" do
|
200
|
+
name 'Actions::RemoteExecution::RunHostJob'
|
201
|
+
refs 'Actions::RemoteExecution::RunHostJob'
|
202
|
+
sections only: %w[webhooks]
|
203
|
+
property :task, object_of: 'Task', desc: 'Returns the task to which this action belongs'
|
204
|
+
property :host_name, String, desc: "Returns the name of the host"
|
205
|
+
property :host_id, Integer, desc: "Returns the id of the host"
|
206
|
+
property :host, object_of: 'Host', desc: "Returns the host"
|
207
|
+
property :job_invocation_id, Integer, desc: "Returns the id of the job invocation"
|
208
|
+
property :job_invocation, object_of: 'JobInvocation', desc: "Returns the job invocation"
|
209
|
+
end
|
210
|
+
class Jail < ::Actions::ObservableAction::Jail
|
211
|
+
allow :host_name, :host_id, :host, :job_invocation_id, :job_invocation
|
212
|
+
end
|
176
213
|
end
|
177
214
|
end
|
178
215
|
end
|
@@ -49,6 +49,13 @@ class HostStatus::ExecutionStatus < HostStatus::Status
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
+
def status_link
|
53
|
+
job_invocation = last_stopped_task.parent_task.job_invocations.first
|
54
|
+
return nil unless User.current.can?(:view_job_invocations, job_invocation)
|
55
|
+
|
56
|
+
Rails.application.routes.url_helpers.job_invocation_path(job_invocation)
|
57
|
+
end
|
58
|
+
|
52
59
|
class ExecutionTaskStatusMapper
|
53
60
|
attr_accessor :task
|
54
61
|
|
data/config/routes.rb
CHANGED
@@ -43,6 +43,9 @@ Rails.application.routes.draw do
|
|
43
43
|
get 'cockpit/host_ssh_params/:id', to: 'cockpit#host_ssh_params'
|
44
44
|
end
|
45
45
|
get 'cockpit/redirect', to: 'cockpit#redirect'
|
46
|
+
get 'ui_job_wizard/categories', to: 'ui_job_wizard#categories'
|
47
|
+
|
48
|
+
match '/experimental/job_wizard', to: 'react#index', :via => [:get]
|
46
49
|
|
47
50
|
namespace :api, :defaults => {:format => 'json'} do
|
48
51
|
scope '(:apiv)', :module => :v2, :defaults => {:apiv => 'v2'}, :apiv => /v1|v2/, :constraints => ApiConstraints.new(:version => 2, :default => true) do
|
@@ -26,9 +26,8 @@ Gem::Specification.new do |s|
|
|
26
26
|
s.add_dependency 'deface'
|
27
27
|
s.add_dependency 'dynflow', '>= 1.0.2', '< 2.0.0'
|
28
28
|
s.add_dependency 'foreman_remote_execution_core'
|
29
|
-
s.add_dependency 'foreman-tasks', '>= 0.
|
29
|
+
s.add_dependency 'foreman-tasks', '>= 4.0.0'
|
30
30
|
|
31
31
|
s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
|
32
|
-
s.add_development_dependency 'rubocop', '~> 0.80.0'
|
33
32
|
s.add_development_dependency 'rdoc'
|
34
33
|
end
|
@@ -53,6 +53,7 @@ module ForemanRemoteExecution
|
|
53
53
|
|
54
54
|
initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
|
55
55
|
Foreman::Plugin.register :foreman_remote_execution do
|
56
|
+
register_global_js_file 'global'
|
56
57
|
requires_foreman '>= 2.2'
|
57
58
|
|
58
59
|
apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
|
@@ -68,7 +69,8 @@ module ForemanRemoteExecution
|
|
68
69
|
permission :view_job_templates, { :job_templates => [:index, :show, :revision, :auto_complete_search, :auto_complete_job_category, :preview, :export],
|
69
70
|
:'api/v2/job_templates' => [:index, :show, :revision, :export],
|
70
71
|
:'api/v2/template_inputs' => [:index, :show],
|
71
|
-
:'api/v2/foreign_input_sets' => [:index, :show]
|
72
|
+
:'api/v2/foreign_input_sets' => [:index, :show],
|
73
|
+
:ui_job_wizard => [:categories]}, :resource_type => 'JobTemplate'
|
72
74
|
permission :create_job_templates, { :job_templates => [:new, :create, :clone_template, :import],
|
73
75
|
:'api/v2/job_templates' => [:create, :clone, :import] }, :resource_type => 'JobTemplate'
|
74
76
|
permission :edit_job_templates, { :job_templates => [:edit, :update],
|
@@ -137,6 +139,13 @@ module ForemanRemoteExecution
|
|
137
139
|
parent: :monitor_menu,
|
138
140
|
after: :audits
|
139
141
|
|
142
|
+
menu :labs_menu, :job_wizard,
|
143
|
+
url_hash: { controller: 'job_wizard', action: :index },
|
144
|
+
caption: N_('Job wizard'),
|
145
|
+
parent: :lab_features_menu,
|
146
|
+
url: 'experimental/job_wizard',
|
147
|
+
after: :host_wizard
|
148
|
+
|
140
149
|
register_custom_status HostStatus::ExecutionStatus
|
141
150
|
# add dashboard widget
|
142
151
|
# widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
|
@@ -157,6 +166,8 @@ module ForemanRemoteExecution
|
|
157
166
|
extend_page 'registration/_form' do |cx|
|
158
167
|
cx.add_pagelet :global_registration, name: N_('Remote Execution'), partial: 'api/v2/registration/form', priority: 100, id: 'remote_execution_interface'
|
159
168
|
end
|
169
|
+
ForemanTasks.dynflow.eager_load_actions!
|
170
|
+
extend_observable_events(::Dynflow::Action.descendants.select { |klass| klass <= ::Actions::ObservableAction }.map(&:namespaced_event_names))
|
160
171
|
end
|
161
172
|
end
|
162
173
|
|
@@ -137,6 +137,7 @@ module Api
|
|
137
137
|
|
138
138
|
test 'should provide empty output for host which does not have a task yet' do
|
139
139
|
JobInvocation.any_instance.expects(:sub_task_for_host).returns(nil)
|
140
|
+
JobInvocation.any_instance.expects(:finished?).returns(false)
|
140
141
|
get :output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
141
142
|
result = ActiveSupport::JSON.decode(@response.body)
|
142
143
|
assert_equal result['refresh'], true
|
@@ -144,6 +145,15 @@ module Api
|
|
144
145
|
assert_response :success
|
145
146
|
end
|
146
147
|
|
148
|
+
test 'should provide empty output marked as done for host which does not have a task when the job is finished' do
|
149
|
+
JobInvocation.any_instance.expects(:sub_task_for_host).returns(nil)
|
150
|
+
get :output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
151
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
152
|
+
assert_equal result['refresh'], false
|
153
|
+
assert_equal result['output'], []
|
154
|
+
assert_response :success
|
155
|
+
end
|
156
|
+
|
147
157
|
test 'should fail with 404 for non-existing job invocation' do
|
148
158
|
invocation_id = @invocation.id + 1
|
149
159
|
assert_empty JobInvocation.where(:id => invocation_id)
|
@@ -166,7 +176,7 @@ module Api
|
|
166
176
|
result = ActiveSupport::JSON.decode(@response.body)
|
167
177
|
host_output = result['outputs'].first
|
168
178
|
assert_equal host_output['host_id'], @invocation.targeting.host_ids.first
|
169
|
-
assert_equal host_output['refresh'],
|
179
|
+
assert_equal host_output['refresh'], false
|
170
180
|
assert_equal host_output['output'], []
|
171
181
|
assert_response :success
|
172
182
|
end
|
@@ -176,7 +186,7 @@ module Api
|
|
176
186
|
result = ActiveSupport::JSON.decode(@response.body)
|
177
187
|
host_output = result['outputs'].first
|
178
188
|
assert_equal host_output['host_id'], @invocation.targeting.host_ids.first
|
179
|
-
assert_equal host_output['refresh'],
|
189
|
+
assert_equal host_output['refresh'], false
|
180
190
|
assert_equal host_output['output'], []
|
181
191
|
assert_response :success
|
182
192
|
end
|
@@ -201,7 +211,7 @@ module Api
|
|
201
211
|
let(:host) { @invocation.targeting.hosts.first }
|
202
212
|
|
203
213
|
test 'should provide raw output for a host' do
|
204
|
-
JobInvocation.any_instance.expects(:task).returns(OpenStruct.new(:scheduled? => false))
|
214
|
+
JobInvocation.any_instance.expects(:task).times(3).returns(OpenStruct.new(:scheduled? => false, :pending? => false))
|
205
215
|
JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
|
206
216
|
get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
207
217
|
result = ActiveSupport::JSON.decode(@response.body)
|
@@ -214,7 +224,7 @@ module Api
|
|
214
224
|
start_time = Time.now
|
215
225
|
JobInvocation.any_instance
|
216
226
|
.expects(:task).twice
|
217
|
-
.returns(OpenStruct.new(:scheduled? => true, :start_at => start_time))
|
227
|
+
.returns(OpenStruct.new(:scheduled? => true, :start_at => start_time, :pending? => true))
|
218
228
|
JobInvocation.any_instance.expects(:sub_task_for_host).never
|
219
229
|
get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
220
230
|
result = ActiveSupport::JSON.decode(@response.body)
|
@@ -226,7 +236,7 @@ module Api
|
|
226
236
|
end
|
227
237
|
|
228
238
|
test 'should provide raw output for host without task' do
|
229
|
-
JobInvocation.any_instance.expects(:task).returns(OpenStruct.new(:scheduled? => false))
|
239
|
+
JobInvocation.any_instance.expects(:task).times(3).returns(OpenStruct.new(:scheduled? => false, :pending? => true))
|
230
240
|
JobInvocation.any_instance.expects(:sub_task_for_host)
|
231
241
|
get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
232
242
|
result = ActiveSupport::JSON.decode(@response.body)
|
@@ -7,11 +7,10 @@ module Api
|
|
7
7
|
describe 'host registration' do
|
8
8
|
let(:organization) { FactoryBot.create(:organization) }
|
9
9
|
let(:tax_location) { FactoryBot.create(:location) }
|
10
|
-
let(:
|
11
|
-
let(:registration_template) do
|
10
|
+
let(:template) do
|
12
11
|
FactoryBot.create(
|
13
12
|
:provisioning_template,
|
14
|
-
template_kind:
|
13
|
+
template_kind: template_kinds(:host_init_config),
|
15
14
|
template: 'template content <%= @host.name %>',
|
16
15
|
locations: [tax_location],
|
17
16
|
organizations: [organization]
|
@@ -23,7 +22,7 @@ module Api
|
|
23
22
|
:with_associations,
|
24
23
|
family: 'Redhat',
|
25
24
|
provisioning_templates: [
|
26
|
-
|
25
|
+
template,
|
27
26
|
]
|
28
27
|
)
|
29
28
|
end
|
@@ -36,17 +35,9 @@ module Api
|
|
36
35
|
operatingsystem_id: os.id } }
|
37
36
|
end
|
38
37
|
|
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
38
|
describe 'remote_execution_interface' do
|
49
39
|
setup do
|
40
|
+
Setting[:default_host_init_config_template] = template.name
|
50
41
|
@host = Host.create(host_params[:host])
|
51
42
|
@interface0 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy0', execution: false)
|
52
43
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
class UiJobWizardControllerTest < ActionController::TestCase
|
4
|
+
def setup
|
5
|
+
FactoryBot.create(:job_template, :job_category => 'cat1')
|
6
|
+
FactoryBot.create(:job_template, :job_category => 'cat2')
|
7
|
+
FactoryBot.create(:job_template, :job_category => 'cat2')
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'should respond with categories' do
|
11
|
+
get :categories, :params => {}, :session => set_session_user
|
12
|
+
assert_response :success
|
13
|
+
res = JSON.parse @response.body
|
14
|
+
assert_equal ['cat1','cat2'], res['job_categories']
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Wizard } from '@patternfly/react-core';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import history from 'foremanReact/history';
|
5
|
+
|
6
|
+
export const JobWizard = () => {
|
7
|
+
const steps = [
|
8
|
+
{
|
9
|
+
name: __('Category and template'),
|
10
|
+
component: <p>Category and template</p>,
|
11
|
+
},
|
12
|
+
{ name: __('Target hosts'), component: <p>TargetHosts </p> },
|
13
|
+
{ name: __('Advanced fields'), component: <p> AdvancedFields </p> },
|
14
|
+
{ name: __('Schedule'), component: <p>Schedule</p> },
|
15
|
+
{
|
16
|
+
name: __('Review details'),
|
17
|
+
component: <p>ReviewDetails</p>,
|
18
|
+
nextButtonText: 'Run',
|
19
|
+
},
|
20
|
+
];
|
21
|
+
const title = __('Run Job');
|
22
|
+
return (
|
23
|
+
<Wizard
|
24
|
+
onClose={() => history.goBack()}
|
25
|
+
navAriaLabel={`${title} steps`}
|
26
|
+
steps={steps}
|
27
|
+
height="70vh"
|
28
|
+
/>
|
29
|
+
);
|
30
|
+
};
|
31
|
+
|
32
|
+
export default JobWizard;
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Title, Divider } from '@patternfly/react-core';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
5
|
+
import { JobWizard } from './JobWizard';
|
6
|
+
|
7
|
+
const JobWizardPage = () => {
|
8
|
+
const title = __('Run job');
|
9
|
+
const breadcrumbOptions = {
|
10
|
+
breadcrumbItems: [
|
11
|
+
{ caption: __('Jobs'), url: `/jobs` },
|
12
|
+
{ caption: title },
|
13
|
+
],
|
14
|
+
};
|
15
|
+
return (
|
16
|
+
<PageLayout
|
17
|
+
header={title}
|
18
|
+
breadcrumbOptions={breadcrumbOptions}
|
19
|
+
searchable={false}
|
20
|
+
>
|
21
|
+
<React.Fragment>
|
22
|
+
<Title headingLevel="h2" size="2xl">
|
23
|
+
{title}
|
24
|
+
</Title>
|
25
|
+
<Divider component="div" />
|
26
|
+
<JobWizard />
|
27
|
+
</React.Fragment>
|
28
|
+
</PageLayout>
|
29
|
+
);
|
30
|
+
};
|
31
|
+
|
32
|
+
export default JobWizardPage;
|
@@ -0,0 +1 @@
|
|
1
|
+
export default { goBack: () => null };
|