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.
data/Gemfile CHANGED
@@ -2,7 +2,4 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec :name => 'foreman_remote_execution'
4
4
 
5
- gem 'rubocop', '~> 0.80.0'
6
- gem 'rubocop-minitest'
7
- gem 'rubocop-performance'
8
- gem 'rubocop-rails'
5
+ gem 'theforeman-rubocop', '~> 0.1.0.pre'
@@ -214,7 +214,7 @@ module Api
214
214
  end
215
215
 
216
216
  def host_output(job_invocation, host, default: nil, since: nil, raw: false)
217
- refresh = true
217
+ refresh = !job_invocation.finished?
218
218
 
219
219
  if (task = job_invocation.sub_task_for_host(host))
220
220
  refresh = task.pending?
@@ -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
 
@@ -241,7 +241,7 @@ class JobInvocation < ApplicationRecord
241
241
  end
242
242
 
243
243
  def finished?
244
- !task.pending?
244
+ !(task.nil? || task.pending?)
245
245
  end
246
246
 
247
247
  def missing_hosts_count
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.15.1'
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]}, :resource_type => 'JobTemplate'
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
 
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '4.2.3'.freeze
2
+ VERSION = '4.3.0'.freeze
3
3
  end
@@ -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'], true
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'], true
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(:template_kind) { template_kinds(:registration) }
11
- let(:registration_template) do
10
+ let(:template) do
12
11
  FactoryBot.create(
13
12
  :provisioning_template,
14
- template_kind: 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
- registration_template,
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,12 @@
1
+ import React from 'react';
2
+ import JobWizardPage from '../JobWizard';
3
+
4
+ const ForemanREXRoutes = [
5
+ {
6
+ path: '/experimental/job_wizard',
7
+ exact: true,
8
+ render: props => <JobWizardPage {...props} />,
9
+ },
10
+ ];
11
+
12
+ export default ForemanREXRoutes;
@@ -0,0 +1 @@
1
+ export default { goBack: () => null };
@@ -0,0 +1,4 @@
1
+ import { registerRoutes } from 'foremanReact/routes/RoutingService';
2
+ import routes from './Routes/routes';
3
+
4
+ registerRoutes('foreman_remote_execution', routes);
@@ -39,7 +39,7 @@ const TargetingHostsPage = ({
39
39
  <br />
40
40
  <TargetingHosts apiStatus={apiStatus} items={items} />
41
41
  <Pagination
42
- viewType="list"
42
+ viewType="table"
43
43
  itemCount={totalHosts}
44
44
  pagination={pagination}
45
45
  onChange={args => handlePagination(args)}
@@ -1,6 +1,3 @@
1
- .targeting-hosts-pagination {
2
- margin-top: -7px;
3
- }
4
1
  #targeting_hosts {
5
2
  min-height: 350px;
6
3
  }
@@ -62,7 +62,7 @@ exports[`TargetingHostsPage renders 1`] = `
62
62
  "perPage": 20,
63
63
  }
64
64
  }
65
- viewType="list"
65
+ viewType="table"
66
66
  />
67
67
  </div>
68
68
  `;