foreman_remote_execution 4.2.0 → 4.3.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/js_ci.yml +29 -0
  3. data/.github/workflows/{ci.yml → ruby_ci.yml} +22 -50
  4. data/.prettierrc +4 -0
  5. data/.rubocop.yml +13 -49
  6. data/.rubocop_todo.yml +326 -102
  7. data/Gemfile +1 -4
  8. data/app/controllers/api/v2/job_invocations_controller.rb +1 -1
  9. data/app/controllers/ui_job_wizard_controller.rb +18 -0
  10. data/app/lib/actions/remote_execution/run_host_job.rb +38 -1
  11. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -0
  12. data/app/models/foreign_input_set.rb +1 -1
  13. data/app/models/host_status/execution_status.rb +7 -0
  14. data/app/models/job_invocation.rb +1 -1
  15. data/app/models/job_invocation_composer.rb +1 -1
  16. data/app/views/template_invocations/show.html.erb +30 -23
  17. data/config/routes.rb +4 -0
  18. data/foreman_remote_execution.gemspec +1 -2
  19. data/lib/foreman_remote_execution/engine.rb +12 -1
  20. data/lib/foreman_remote_execution/version.rb +1 -1
  21. data/lib/tasks/foreman_remote_execution_tasks.rake +1 -18
  22. data/test/functional/api/v2/job_invocations_controller_test.rb +24 -4
  23. data/test/functional/api/v2/registration_controller_test.rb +4 -13
  24. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  25. data/webpack/JobWizard/JobWizard.js +32 -0
  26. data/webpack/JobWizard/index.js +32 -0
  27. data/webpack/Routes/routes.js +12 -0
  28. data/webpack/__mocks__/foremanReact/history.js +1 -0
  29. data/webpack/global_index.js +4 -0
  30. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -1
  31. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  32. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  33. metadata +15 -19
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[all 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
@@ -6,6 +6,7 @@ module ForemanRemoteExecution
6
6
  extend ApipieDSL::Class
7
7
 
8
8
  attr_reader :template, :host, :invocation, :input_template_instance, :current_user
9
+ attr_accessor :error_message
9
10
  delegate :input, to: :input_template_instance
10
11
 
11
12
  apipie :class, 'Macros related to template rendering' do
@@ -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.template.name }
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'
@@ -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
@@ -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,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
- <% breadcrumbs(:resource_url => template_invocations_api_job_invocation_path(@template_invocation.job_invocation_id),
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
- <h3><%= _('Target: ') %><%= link_to(@host.name, host_path(@host)) %></h3>
27
-
28
- <div class="preview hidden">
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
- <% if @error %>
34
- <div class="line error"><%= @error %></div>
35
- <% else %>
36
- <%= link_to_function(_('Scroll to bottom'), '$("html, body").animate({ scrollTop: $(document).height() }, "slow");', :class => 'pull-right scroll-link-bottom') %>
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
- <div class="printable">
39
- <%= render :partial => 'output_line_set', :collection => normalize_line_sets(@line_sets) %>
40
- </div>
42
+ <div class="printable">
43
+ <%= render :partial => 'output_line_set', :collection => normalize_line_sets(@line_sets) %>
44
+ </div>
41
45
 
42
- <%= link_to_function(_('Scroll to top'), '$("html, body").animate({ scrollTop: 0 }, "slow");', :class => 'pull-right scroll-link-top') %>
43
- <% end %>
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
- <%= render :partial => 'refresh.js' %>
48
- </script>
50
+ <script>
51
+ <%= render :partial => 'refresh.js' %>
52
+ </script>
53
+ <% else %>
54
+ <%= _("Could not display data for job invocation.") %>
55
+ <% end %>
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
@@ -56,6 +59,7 @@ Rails.application.routes.draw do
56
59
  post 'rerun'
57
60
  get 'template_invocations', :to => 'template_invocations#template_invocations'
58
61
  get 'outputs'
62
+ post 'outputs'
59
63
  end
60
64
  end
61
65
 
@@ -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.0'.freeze
2
+ VERSION = '4.3.1'.freeze
3
3
  end
@@ -20,26 +20,9 @@ namespace :test do
20
20
  end
21
21
  end
22
22
 
23
- namespace :foreman_remote_execution do
24
- task :rubocop do
25
- begin
26
- require 'rubocop/rake_task'
27
- RuboCop::RakeTask.new(:rubocop_foreman_remote_execution) do |task|
28
- task.patterns = ["#{ForemanRemoteExecution::Engine.root}/app/**/*.rb",
29
- "#{ForemanRemoteExecution::Engine.root}/lib/**/*.rb",
30
- "#{ForemanRemoteExecution::Engine.root}/test/**/*.rb"]
31
- end
32
- rescue
33
- puts 'Rubocop not loaded.'
34
- end
35
-
36
- Rake::Task['rubocop_foreman_remote_execution'].invoke
37
- end
38
- end
39
-
40
23
  Rake::Task[:test].enhance ['test:foreman_remote_execution']
41
24
 
42
25
  load 'tasks/jenkins.rake'
43
26
  if Rake::Task.task_defined?(:'jenkins:unit')
44
- Rake::Task['jenkins:unit'].enhance ['test:foreman_remote_execution', 'foreman_remote_execution:rubocop']
27
+ Rake::Task['jenkins:unit'].enhance ['test:foreman_remote_execution']
45
28
  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,17 @@ 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
180
+ assert_equal host_output['output'], []
181
+ assert_response :success
182
+ end
183
+
184
+ test 'should provide outputs for selected hosts in the job' do
185
+ post :outputs, params: { :id => @invocation.id, :search_query => "id = #{@invocation.targeting.host_ids.first}" }, as: :json
186
+ result = ActiveSupport::JSON.decode(@response.body)
187
+ host_output = result['outputs'].first
188
+ assert_equal host_output['host_id'], @invocation.targeting.host_ids.first
189
+ assert_equal host_output['refresh'], false
170
190
  assert_equal host_output['output'], []
171
191
  assert_response :success
172
192
  end
@@ -191,7 +211,7 @@ module Api
191
211
  let(:host) { @invocation.targeting.hosts.first }
192
212
 
193
213
  test 'should provide raw output for a host' do
194
- 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))
195
215
  JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
196
216
  get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
197
217
  result = ActiveSupport::JSON.decode(@response.body)
@@ -204,7 +224,7 @@ module Api
204
224
  start_time = Time.now
205
225
  JobInvocation.any_instance
206
226
  .expects(:task).twice
207
- .returns(OpenStruct.new(:scheduled? => true, :start_at => start_time))
227
+ .returns(OpenStruct.new(:scheduled? => true, :start_at => start_time, :pending? => true))
208
228
  JobInvocation.any_instance.expects(:sub_task_for_host).never
209
229
  get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
210
230
  result = ActiveSupport::JSON.decode(@response.body)
@@ -216,7 +236,7 @@ module Api
216
236
  end
217
237
 
218
238
  test 'should provide raw output for host without task' do
219
- 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))
220
240
  JobInvocation.any_instance.expects(:sub_task_for_host)
221
241
  get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
222
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