foreman_remote_execution 4.2.3 → 4.3.0
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/.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 };
|