foreman_remote_execution 4.1.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) 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 +21 -3
  9. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +26 -0
  10. data/app/controllers/job_templates_controller.rb +1 -1
  11. data/app/controllers/ui_job_wizard_controller.rb +18 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +38 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +9 -1
  14. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +38 -14
  15. data/app/models/foreign_input_set.rb +1 -1
  16. data/app/models/host_status/execution_status.rb +7 -0
  17. data/app/models/job_invocation.rb +2 -1
  18. data/app/models/job_invocation_composer.rb +1 -1
  19. data/app/models/remote_execution_feature.rb +5 -2
  20. data/app/models/remote_execution_provider.rb +6 -1
  21. data/app/services/remote_execution_proxy_selector.rb +3 -0
  22. data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
  23. data/app/views/api/v2/registration/_form.html.erb +12 -0
  24. data/app/views/template_invocations/_output_line_set.html.erb +1 -1
  25. data/app/views/template_invocations/show.html.erb +30 -23
  26. data/app/views/templates/ssh/package_action.erb +1 -0
  27. data/config/routes.rb +5 -0
  28. data/db/migrate/20200820122057_add_proxy_selector_override_to_remote_execution_feature.rb +5 -0
  29. data/foreman_remote_execution.gemspec +1 -2
  30. data/lib/foreman_remote_execution/engine.rb +21 -2
  31. data/lib/foreman_remote_execution/version.rb +1 -1
  32. data/package.json +6 -6
  33. data/test/functional/api/v2/job_invocations_controller_test.rb +42 -3
  34. data/test/functional/api/v2/registration_controller_test.rb +73 -0
  35. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  36. data/test/unit/actions/run_hosts_job_test.rb +1 -0
  37. data/webpack/JobWizard/JobWizard.js +32 -0
  38. data/webpack/JobWizard/index.js +32 -0
  39. data/webpack/Routes/routes.js +12 -0
  40. data/webpack/__mocks__/foremanReact/history.js +1 -0
  41. data/webpack/global_index.js +4 -0
  42. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +5 -1
  43. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +6 -2
  44. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  45. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  46. metadata +24 -23
@@ -98,6 +98,7 @@ handle_zypp_res_codes () {
98
98
  end
99
99
  -%>
100
100
  [ -x "$(command -v subscription-manager)" ] && subscription-manager refresh
101
+ export DEBIAN_FRONTEND=noninteractive
101
102
  apt-get -y update
102
103
  apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y <%= action %> <%= input("package") %>
103
104
  <% elsif package_manager == 'zypper' -%>
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
@@ -55,6 +58,8 @@ Rails.application.routes.draw do
55
58
  post 'cancel'
56
59
  post 'rerun'
57
60
  get 'template_invocations', :to => 'template_invocations#template_invocations'
61
+ get 'outputs'
62
+ post 'outputs'
58
63
  end
59
64
  end
60
65
 
@@ -0,0 +1,5 @@
1
+ class AddProxySelectorOverrideToRemoteExecutionFeature < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :remote_execution_features, :proxy_selector_override, :string
4
+ end
5
+ end
@@ -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],
@@ -83,7 +85,7 @@ module ForemanRemoteExecution
83
85
  permission :create_job_invocations, { :job_invocations => [:new, :create, :refresh, :rerun, :preview_hosts],
84
86
  'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
85
87
  permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search], :template_invocations => [:show],
86
- 'api/v2/job_invocations' => [:index, :show, :output, :raw_output] }, :resource_type => 'JobInvocation'
88
+ 'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs] }, :resource_type => 'JobInvocation'
87
89
  permission :view_template_invocations, { :template_invocations => [:show],
88
90
  'api/v2/template_invocations' => [:template_invocations] }, :resource_type => 'TemplateInvocation'
89
91
  permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
@@ -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
@@ -151,6 +160,14 @@ module ForemanRemoteExecution
151
160
  extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies'
152
161
  parameter_filter ::Subnet, :remote_execution_proxy_ids
153
162
  describe_host { overview_buttons_provider :host_overview_buttons }
163
+
164
+ # Extend Registration module
165
+ extend_allowed_registration_vars :remote_execution_interface
166
+ extend_page 'registration/_form' do |cx|
167
+ cx.add_pagelet :global_registration, name: N_('Remote Execution'), partial: 'api/v2/registration/form', priority: 100, id: 'remote_execution_interface'
168
+ end
169
+ ForemanTasks.dynflow.eager_load_actions!
170
+ extend_observable_events(::Dynflow::Action.descendants.select { |klass| klass <= ::Actions::ObservableAction }.map(&:namespaced_event_names))
154
171
  end
155
172
  end
156
173
 
@@ -207,6 +224,8 @@ module ForemanRemoteExecution
207
224
  ForemanRemoteExecution.register_rex_feature
208
225
 
209
226
  ::Api::V2::SubnetsController.include ::ForemanRemoteExecution::Concerns::Api::V2::SubnetsControllerExtensions
227
+ ::Api::V2::RegistrationController.prepend ::ForemanRemoteExecution::Concerns::Api::V2::RegistrationControllerExtensions
228
+ ::Api::V2::RegistrationController.include ::ForemanRemoteExecution::Concerns::Api::V2::RegistrationControllerExtensions::ApipieExtensions
210
229
  end
211
230
 
212
231
  initializer 'foreman_remote_execution.register_gettext', after: :load_config_initializers do |_app|
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '4.1.0'.freeze
2
+ VERSION = '4.3.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -21,16 +21,16 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@babel/core": "^7.7.0",
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",
24
+ "@theforeman/builder": "^4.14.0",
25
+ "@theforeman/eslint-plugin-foreman": "^4.14.0",
26
+ "@theforeman/stories": "^4.14.0",
27
+ "@theforeman/test": "^4.14.0",
28
+ "@theforeman/vendor-dev": "^4.14.0",
29
29
  "babel-eslint": "^10.0.0",
30
30
  "eslint": "^6.8.0",
31
31
  "prettier": "^1.19.1"
32
32
  },
33
33
  "peerDependencies": {
34
- "@theforeman/vendor": ">= 4.2.1"
34
+ "@theforeman/vendor": ">= 4.14.0"
35
35
  }
36
36
  }
@@ -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)
@@ -160,6 +170,35 @@ module Api
160
170
  end
161
171
  end
162
172
 
173
+ describe '#outputs' do
174
+ test 'should provide outputs for hosts in the job' do
175
+ get :outputs, params: { :id => @invocation.id }
176
+ result = ActiveSupport::JSON.decode(@response.body)
177
+ host_output = result['outputs'].first
178
+ assert_equal host_output['host_id'], @invocation.targeting.host_ids.first
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
190
+ assert_equal host_output['output'], []
191
+ assert_response :success
192
+ end
193
+
194
+ test 'should provide outputs for hosts in the job matching a search query' do
195
+ get :outputs, params: { :id => @invocation.id, :search_query => "name = definitely_not_in_the_job" }
196
+ result = ActiveSupport::JSON.decode(@response.body)
197
+ assert_equal result['outputs'], []
198
+ assert_response :success
199
+ end
200
+ end
201
+
163
202
  describe 'raw output' do
164
203
  let(:fake_output) do
165
204
  (1..5).map do |i|
@@ -172,7 +211,7 @@ module Api
172
211
  let(:host) { @invocation.targeting.hosts.first }
173
212
 
174
213
  test 'should provide raw output for a host' do
175
- 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))
176
215
  JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
177
216
  get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
178
217
  result = ActiveSupport::JSON.decode(@response.body)
@@ -185,7 +224,7 @@ module Api
185
224
  start_time = Time.now
186
225
  JobInvocation.any_instance
187
226
  .expects(:task).twice
188
- .returns(OpenStruct.new(:scheduled? => true, :start_at => start_time))
227
+ .returns(OpenStruct.new(:scheduled? => true, :start_at => start_time, :pending? => true))
189
228
  JobInvocation.any_instance.expects(:sub_task_for_host).never
190
229
  get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
191
230
  result = ActiveSupport::JSON.decode(@response.body)
@@ -197,7 +236,7 @@ module Api
197
236
  end
198
237
 
199
238
  test 'should provide raw output for host without task' do
200
- 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))
201
240
  JobInvocation.any_instance.expects(:sub_task_for_host)
202
241
  get :raw_output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
203
242
  result = ActiveSupport::JSON.decode(@response.body)
@@ -0,0 +1,73 @@
1
+ require 'test_plugin_helper'
2
+
3
+ module Api
4
+ module V2
5
+ # Tests for the extra methods to play roles on a Host
6
+ class RegistrationControllerTest < ActionController::TestCase
7
+ describe 'host registration' do
8
+ let(:organization) { FactoryBot.create(:organization) }
9
+ let(:tax_location) { FactoryBot.create(:location) }
10
+ let(:template) do
11
+ FactoryBot.create(
12
+ :provisioning_template,
13
+ template_kind: template_kinds(:host_init_config),
14
+ template: 'template content <%= @host.name %>',
15
+ locations: [tax_location],
16
+ organizations: [organization]
17
+ )
18
+ end
19
+ let(:os) do
20
+ FactoryBot.create(
21
+ :operatingsystem,
22
+ :with_associations,
23
+ family: 'Redhat',
24
+ provisioning_templates: [
25
+ template,
26
+ ]
27
+ )
28
+ end
29
+
30
+ let(:host_params) do
31
+ { host: { name: 'centos-test.example.com',
32
+ managed: false, build: false,
33
+ organization_id: organization.id,
34
+ location_id: tax_location.id,
35
+ operatingsystem_id: os.id } }
36
+ end
37
+
38
+ describe 'remote_execution_interface' do
39
+ setup do
40
+ Setting[:default_host_init_config_template] = template.name
41
+ @host = Host.create(host_params[:host])
42
+ @interface0 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy0', execution: false)
43
+ end
44
+
45
+ test 'with existing interface' do
46
+ params = host_params.merge(remote_execution_interface: @interface0.identifier)
47
+
48
+ post :host, params: params, session: set_session_user
49
+ assert_response :success
50
+ assert @interface0.reload.execution
51
+ end
52
+
53
+ test 'with not-existing interface' do
54
+ params = host_params.merge(remote_execution_interface: 'dummy999')
55
+
56
+ post :host, params: params, session: set_session_user
57
+ assert_response :not_found
58
+ end
59
+
60
+ test 'with multiple interfaces' do
61
+ interface1 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy1', execution: false)
62
+ params = host_params.merge(remote_execution_interface: interface1.identifier)
63
+
64
+ post :host, params: params, session: set_session_user
65
+ assert_response :success
66
+ refute @interface0.reload.execution
67
+ assert interface1.reload.execution
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ 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
@@ -24,6 +24,7 @@ module ForemanRemoteExecution
24
24
  OpenStruct.new(:id => uuid).tap do |o|
25
25
  o.stubs(:add_missing_task_groups)
26
26
  o.stubs(:task_groups).returns([])
27
+ o.stubs(:pending?).returns(true)
27
28
  end
28
29
  end
29
30
  let(:action) do
@@ -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);
@@ -51,8 +51,12 @@ const TargetingHosts = ({ apiStatus, items }) => {
51
51
  };
52
52
 
53
53
  TargetingHosts.propTypes = {
54
- apiStatus: PropTypes.string.isRequired,
54
+ apiStatus: PropTypes.string,
55
55
  items: PropTypes.array.isRequired,
56
56
  };
57
57
 
58
+ TargetingHosts.defaultProps = {
59
+ apiStatus: null,
60
+ };
61
+
58
62
  export default TargetingHosts;
@@ -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)}
@@ -52,11 +52,15 @@ const TargetingHostsPage = ({
52
52
  TargetingHostsPage.propTypes = {
53
53
  handleSearch: PropTypes.func.isRequired,
54
54
  searchQuery: PropTypes.string.isRequired,
55
- apiStatus: PropTypes.string.isRequired,
55
+ apiStatus: PropTypes.string,
56
56
  items: PropTypes.array.isRequired,
57
57
  totalHosts: PropTypes.number.isRequired,
58
58
  pagination: PropTypes.object.isRequired,
59
59
  handlePagination: PropTypes.func.isRequired,
60
60
  };
61
61
 
62
+ TargetingHostsPage.defaultProps = {
63
+ apiStatus: null,
64
+ };
65
+
62
66
  export default TargetingHostsPage;