foreman_remote_execution 4.5.5 → 4.8.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
  5. data/app/graphql/types/job_invocation.rb +16 -0
  6. data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  8. data/app/mailers/rex_job_mailer.rb +15 -0
  9. data/app/models/job_invocation.rb +4 -0
  10. data/app/models/job_invocation_composer.rb +21 -13
  11. data/app/models/job_template.rb +1 -1
  12. data/app/models/remote_execution_provider.rb +17 -2
  13. data/app/models/rex_mail_notification.rb +13 -0
  14. data/app/models/setting/remote_execution.rb +7 -1
  15. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  16. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  17. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  18. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  19. data/app/views/template_invocations/show.html.erb +2 -1
  20. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  21. data/db/seeds.d/95-mail_notifications.rb +24 -0
  22. data/foreman_remote_execution.gemspec +2 -4
  23. data/lib/foreman_remote_execution/engine.rb +4 -0
  24. data/lib/foreman_remote_execution/version.rb +1 -1
  25. data/package.json +6 -6
  26. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  27. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  28. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  29. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  30. data/test/unit/concerns/host_extensions_test.rb +4 -4
  31. data/test/unit/input_template_renderer_test.rb +1 -89
  32. data/test/unit/job_invocation_composer_test.rb +1 -12
  33. data/test/unit/job_invocation_report_template_test.rb +15 -12
  34. data/test/unit/remote_execution_provider_test.rb +34 -0
  35. data/webpack/JobWizard/JobWizard.js +53 -20
  36. data/webpack/JobWizard/JobWizard.scss +33 -4
  37. data/webpack/JobWizard/JobWizardConstants.js +17 -0
  38. data/webpack/JobWizard/__tests__/fixtures.js +8 -0
  39. data/webpack/JobWizard/__tests__/integration.test.js +3 -7
  40. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
  41. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  45. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
  46. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  47. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
  50. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  51. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
  52. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
  53. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
  54. data/webpack/JobWizard/steps/Schedule/index.js +89 -28
  55. data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
  56. data/webpack/JobWizard/steps/form/Formatter.js +10 -9
  57. data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
  58. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  59. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  60. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  61. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  62. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  63. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  64. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  65. metadata +26 -19
  66. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -18,7 +18,8 @@ end
18
18
 
19
19
  <div id="title_action">
20
20
  <div class="btn-toolbar pull-right">
21
- <%= link_to(_('Back to Job'), job_invocation_path(@template_invocation.job_invocation), :class => 'btn btn-default') %>
21
+ <%= button_group(link_to(_('Back to Job'), job_invocation_path(@template_invocation.job_invocation), :class => 'btn btn-default'),
22
+ (link_to(_('Rerun'), rerun_job_invocation_path(@template_invocation.job_invocation, :host_ids => [ @host.id ]), :class => 'btn btn-default') if authorized_for(:permission => :create_job_invocations))) %>
22
23
  <%= button_group(link_to_function(_('Toggle command'), '$("div.preview").toggle()', :class => 'btn btn-default'),
23
24
  link_to_function(_('Toggle STDERR'), '$("div.line.stderr").toggle()', :class => 'btn btn-default'),
24
25
  link_to_function(_('Toggle STDOUT'), '$("div.line.stdout").toggle()', :class => 'btn btn-default'),
@@ -13,6 +13,20 @@ blueprints = [
13
13
  ],
14
14
  },
15
15
  },
16
+ {
17
+ group: N_('Jobs'),
18
+ name: 'rex_job_failed',
19
+ message: N_("A job '%{subject}' has failed"),
20
+ level: 'error',
21
+ actions:
22
+ {
23
+ links:
24
+ [
25
+ path_method: :job_invocation_path,
26
+ title: N_('Job Details'),
27
+ ],
28
+ },
29
+ },
16
30
  ]
17
31
 
18
32
  blueprints.each { |blueprint| UINotifications::Seed.new(blueprint).configure }
@@ -0,0 +1,24 @@
1
+ N_('Remote execution job')
2
+
3
+ notifications = [
4
+ {
5
+ :name => 'remote_execution_job',
6
+ :description => N_('A notification when a job finishes'),
7
+ :mailer => 'RexJobMailer',
8
+ :method => 'job_finished',
9
+ :subscription_type => 'alert',
10
+ },
11
+ ]
12
+
13
+ notifications.each do |notification|
14
+ if (mail = RexMailNotification.find_by(name: notification[:name]))
15
+ mail.attributes = notification
16
+ mail.save! if mail.changed?
17
+ else
18
+ created_notification = RexMailNotification.create(notification)
19
+ if created_notification.nil? || created_notification.errors.any?
20
+ raise ::Foreman::Exception.new(N_("Unable to create mail notification: %s"),
21
+ format_errors(created_notification))
22
+ end
23
+ end
24
+ end
@@ -16,8 +16,7 @@ Gem::Specification.new do |s|
16
16
  'management functionality with remote management functionality.'
17
17
 
18
18
  s.files = `git ls-files`.split("\n").reject do |file|
19
- file.start_with?('scripts', 'lib/foreman_remote_execution_core') ||
20
- file == 'foreman_remote_execution_core.gemspec'
19
+ file.start_with?('scripts')
21
20
  end
22
21
 
23
22
  s.test_files = `git ls-files test`.split("\n")
@@ -25,8 +24,7 @@ Gem::Specification.new do |s|
25
24
 
26
25
  s.add_dependency 'deface'
27
26
  s.add_dependency 'dynflow', '>= 1.0.2', '< 2.0.0'
28
- s.add_dependency 'foreman_remote_execution_core'
29
- s.add_dependency 'foreman-tasks', '>= 4.1.0'
27
+ s.add_dependency 'foreman-tasks', '>= 5.1.0'
30
28
 
31
29
  s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
32
30
  s.add_development_dependency 'rdoc'
@@ -146,12 +146,16 @@ module ForemanRemoteExecution
146
146
  register_custom_status HostStatus::ExecutionStatus
147
147
  # add dashboard widget
148
148
  # widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
149
+ widget 'dashboard/latest-jobs', :name => N_('Latest Jobs'), :sizex => 6, :sizey => 1
149
150
 
150
151
  parameter_filter Subnet, :remote_execution_proxies, :remote_execution_proxy_ids => []
151
152
  parameter_filter Nic::Interface do |ctx|
152
153
  ctx.permit :execution
153
154
  end
154
155
 
156
+ register_graphql_query_field :job_invocations, '::Types::JobInvocation', :collection_field
157
+ register_graphql_query_field :job_invocation, '::Types::JobInvocation', :record_field
158
+
155
159
  extend_template_helpers ForemanRemoteExecution::RendererMethods
156
160
 
157
161
  extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '4.5.5'.freeze
2
+ VERSION = '4.8.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -21,11 +21,11 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@babel/core": "^7.7.0",
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",
24
+ "@theforeman/builder": "^8.10.0",
25
+ "@theforeman/eslint-plugin-foreman": "^8.10.0",
26
+ "@theforeman/stories": "^8.10.0",
27
+ "@theforeman/test": "^8.10.0",
28
+ "@theforeman/vendor-dev": "^8.10.0",
29
29
  "babel-eslint": "^10.0.0",
30
30
  "eslint": "^6.8.0",
31
31
  "prettier": "^1.19.1",
@@ -33,6 +33,6 @@
33
33
  "redux-mock-store": "^1.2.2"
34
34
  },
35
35
  "peerDependencies": {
36
- "@theforeman/vendor": "^8.3.0"
36
+ "@theforeman/vendor": "^8.10.0"
37
37
  }
38
38
  }
@@ -90,6 +90,16 @@ module Api
90
90
  assert_response :success
91
91
  end
92
92
 
93
+ test 'should create with a scheduled recurrence' do
94
+ @attrs[:scheduling] = { start_at: (Time.now + 1.hour) }
95
+ @attrs[:recurrence] = { cron_line: '5 * * * *' }
96
+ post :create, params: { job_invocation: @attrs }
97
+ invocation = ActiveSupport::JSON.decode(@response.body)
98
+ assert_equal 'recurring', invocation['mode']
99
+ assert invocation['start_at']
100
+ assert_response :success
101
+ end
102
+
93
103
  context 'with_feature' do
94
104
  setup do
95
105
  @feature = FactoryBot.create(:remote_execution_feature,
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ module Queries
4
+ class JobInvocationQueryTest < GraphQLQueryTestCase
5
+ let(:query) do
6
+ <<-GRAPHQL
7
+ query (
8
+ $id: String!
9
+ ) {
10
+ jobInvocation(id: $id) {
11
+ id
12
+ jobCategory
13
+ }
14
+ }
15
+ GRAPHQL
16
+ end
17
+
18
+ let(:job_invocation) { FactoryBot.create(:job_invocation) }
19
+
20
+ let(:global_id) { Foreman::GlobalId.for(job_invocation) }
21
+ let(:variables) { { id: global_id } }
22
+ let(:data) { result['data']['jobInvocation'] }
23
+
24
+ test 'fetching job invocation attributes' do
25
+ assert_empty result['errors']
26
+
27
+ assert_equal global_id, data['id']
28
+ assert_equal job_invocation.job_category, data['jobCategory']
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_plugin_helper'
2
+
3
+ module Queries
4
+ class JobInvocationsQueryTest < GraphQLQueryTestCase
5
+ let(:query) do
6
+ <<-GRAPHQL
7
+ query {
8
+ jobInvocations {
9
+ totalCount
10
+ nodes {
11
+ id
12
+ jobCategory
13
+ }
14
+ }
15
+ }
16
+ GRAPHQL
17
+ end
18
+
19
+ let(:data) { result['data']['jobInvocations'] }
20
+
21
+ setup do
22
+ FactoryBot.create_list(:job_invocation, 2)
23
+ end
24
+
25
+ test 'should fetch job invocations' do
26
+ assert_empty result['errors']
27
+
28
+ expected_count = JobInvocation.count
29
+
30
+ assert_not_equal 0, expected_count
31
+ assert_equal expected_count, data['totalCount']
32
+ assert_equal expected_count, data['nodes'].count
33
+ end
34
+ end
35
+ end
@@ -5,6 +5,16 @@ module ForemanRemoteExecution
5
5
  class RunHostsJobTest < ActiveSupport::TestCase
6
6
  include Dynflow::Testing
7
7
 
8
+ # Adding run_step_id wich is needed in RunHostsJob as a quick fix
9
+ # it will be added to dynflow in the future see https://github.com/Dynflow/dynflow/pull/391
10
+ # rubocop:disable Style/ClassAndModuleChildren
11
+ class Dynflow::Testing::DummyPlannedAction
12
+ def run_step_id
13
+ Dynflow::Testing.get_id
14
+ end
15
+ end
16
+ # rubocop:enable Style/ClassAndModuleChildren
17
+
8
18
  let(:host) { FactoryBot.create(:host, :with_execution) }
9
19
  let(:proxy) { host.remote_execution_proxies('SSH')[:subnet].first }
10
20
  let(:targeting) { FactoryBot.create(:targeting, :search_query => "name = #{host.name}", :user => User.current) }
@@ -94,6 +104,22 @@ module ForemanRemoteExecution
94
104
  planned # To make the expectations happy
95
105
  end
96
106
 
107
+ describe '#proxy_batch_size' do
108
+ it 'defaults to Setting[foreman_tasks_proxy_batch_size]' do
109
+ Setting.expects(:[]).with('foreman_tasks_proxy_batch_size').returns(14)
110
+ planned
111
+ _(planned.proxy_batch_size).must_equal 14
112
+ end
113
+
114
+ it 'gets the provider value' do
115
+ provider = mock('provider')
116
+ provider.expects(:proxy_batch_size).returns(15)
117
+ JobTemplate.any_instance.expects(:provider).returns(provider)
118
+
119
+ _(planned.proxy_batch_size).must_equal 15
120
+ end
121
+ end
122
+
97
123
  describe 'concurrency control' do
98
124
  let(:level) { 5 }
99
125
  let(:span) { 60 }
@@ -127,10 +153,79 @@ module ForemanRemoteExecution
127
153
  end
128
154
 
129
155
  describe 'notifications' do
130
- it 'creates notification on sucess run' do
131
- FactoryBot.create(:notification_blueprint, :name => 'rex_job_succeeded')
132
- assert_difference 'NotificationRecipient.where(:user_id => targeting.user.id).count' do
133
- finalize_action planned
156
+ it 'creates drawer notification on succeess' do
157
+ blueprint = planned.job_invocation.build_notification
158
+ blueprint.expects(:deliver!)
159
+ planned.job_invocation.expects(:build_notification).returns(blueprint)
160
+ planned.notify_on_success(nil)
161
+ end
162
+
163
+ it 'creates drawer notification on failure' do
164
+ blueprint = planned.job_invocation.build_notification
165
+ blueprint.expects(:deliver!)
166
+ planned.job_invocation.expects(:build_notification).returns(blueprint)
167
+ planned.notify_on_failure(nil)
168
+ end
169
+
170
+ describe 'ignoring drawer notification' do
171
+ before do
172
+ blueprint = planned.job_invocation.build_notification
173
+ blueprint.expects(:deliver!)
174
+ planned.job_invocation.expects(:build_notification).returns(blueprint)
175
+ end
176
+
177
+ let(:mail) do
178
+ object = mock
179
+ object.stubs(:deliver_now)
180
+ object
181
+ end
182
+
183
+ describe 'for user subscribed to all' do
184
+ before do
185
+ planned.expects(:mail_notification_preference).returns(UserMailNotification.new(:interval => RexMailNotification::ALL_JOBS))
186
+ end
187
+
188
+ it 'sends the mail notification on success' do
189
+ RexJobMailer.expects(:job_finished).returns(mail)
190
+ planned.notify_on_success(nil)
191
+ end
192
+
193
+ it 'sends the mail notification on failure' do
194
+ RexJobMailer.expects(:job_finished).returns(mail)
195
+ planned.notify_on_failure(nil)
196
+ end
197
+ end
198
+
199
+ describe 'for user subscribed to failures' do
200
+ before do
201
+ planned.expects(:mail_notification_preference).returns(UserMailNotification.new(:interval => RexMailNotification::FAILED_JOBS))
202
+ end
203
+
204
+ it 'it does not send the mail notification on success' do
205
+ RexJobMailer.expects(:job_finished).never
206
+ planned.notify_on_success(nil)
207
+ end
208
+
209
+ it 'sends the mail notification on failure' do
210
+ RexJobMailer.expects(:job_finished).returns(mail)
211
+ planned.notify_on_failure(nil)
212
+ end
213
+ end
214
+
215
+ describe 'for user subscribed to successful jobs' do
216
+ before do
217
+ planned.expects(:mail_notification_preference).returns(UserMailNotification.new(:interval => RexMailNotification::SUCCEEDED_JOBS))
218
+ end
219
+
220
+ it 'sends the mail notification on success' do
221
+ RexJobMailer.expects(:job_finished).returns(mail)
222
+ planned.notify_on_success(nil)
223
+ end
224
+
225
+ it 'does not send the mail notification on failure' do
226
+ RexJobMailer.expects(:job_finished).never
227
+ planned.notify_on_failure(nil)
228
+ end
134
229
  end
135
230
  end
136
231
  end
@@ -131,23 +131,23 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
131
131
  end
132
132
 
133
133
  context 'fallback strategy' do
134
- let(:host) { FactoryBot.build(:host, :with_puppet) }
134
+ let(:host) { FactoryBot.build(:host, :with_tftp_subnet) }
135
135
 
136
136
  context 'enabled' do
137
137
  before do
138
138
  Setting[:remote_execution_fallback_proxy] = true
139
- host.puppet_proxy.features << FactoryBot.create(:feature, :ssh)
139
+ host.subnet.tftp.features << FactoryBot.create(:feature, :ssh)
140
140
  end
141
141
 
142
142
  it 'returns a fallback proxy' do
143
- host.remote_execution_proxies(provider)[:fallback].must_include host.puppet_proxy
143
+ host.remote_execution_proxies(provider)[:fallback].must_include host.subnet.tftp
144
144
  end
145
145
  end
146
146
 
147
147
  context 'disabled' do
148
148
  before do
149
149
  Setting[:remote_execution_fallback_proxy] = false
150
- host.puppet_proxy.features << FactoryBot.create(:feature, :ssh)
150
+ host.subnet.tftp.features << FactoryBot.create(:feature, :ssh)
151
151
  end
152
152
 
153
153
  it 'returns no proxy' do
@@ -446,8 +446,7 @@ class InputTemplateRendererTest < ActiveSupport::TestCase
446
446
  before { User.current = FactoryBot.build(:user, :admin) }
447
447
  after { User.current = nil }
448
448
 
449
- let(:environment) { FactoryBot.create(:environment) }
450
- before { renderer.host = FactoryBot.create(:host, :environment => environment) }
449
+ before { renderer.host = FactoryBot.create(:host) }
451
450
 
452
451
  describe 'rendering' do
453
452
  it 'can\'t render the content without host since we don\'t have variable value in classification' do
@@ -497,91 +496,4 @@ class InputTemplateRendererTest < ActiveSupport::TestCase
497
496
  end
498
497
  end
499
498
  end
500
-
501
- context 'renderer for template with puppet parameter input used' do
502
- let(:template) { FactoryBot.build(:job_template, :template => 'echo "This is WebServer with nginx <%= input("nginx_version") -%>" > /etc/motd') }
503
- let(:renderer) { InputTemplateRenderer.new(template) }
504
-
505
- context 'with matching input defined' do
506
- before do
507
- renderer.template.template_inputs<< FactoryBot.build(:template_input,
508
- :name => 'nginx_version',
509
- :input_type => 'puppet_parameter',
510
- :puppet_parameter_name => 'version',
511
- :puppet_class_name => 'nginx')
512
- end
513
- let(:result) { renderer.render }
514
-
515
- describe 'rendering' do
516
- it 'can\'t render the content without host since we don\'t have host so no classification' do
517
- assert_not result
518
- end
519
-
520
- it 'registers an error' do
521
- result # let is lazy
522
- _(renderer.error_message).wont_be_nil
523
- _(renderer.error_message).wont_be_empty
524
- end
525
-
526
- context 'with host specified' do
527
- let(:environment) { FactoryBot.create(:environment) }
528
- before { renderer.host = FactoryBot.create(:host, :environment => environment) }
529
-
530
- describe 'rendering' do
531
- it 'can\'t render the content without host since we don\'t have puppet parameter in classification' do
532
- assert_not result
533
- end
534
-
535
- it 'registers an error' do
536
- result # let is lazy
537
- _(renderer.error_message).wont_be_nil
538
- _(renderer.error_message).wont_be_empty
539
- end
540
- end
541
-
542
- describe 'preview' do
543
- it 'should render preview' do
544
- _(renderer.preview).must_equal 'echo "This is WebServer with nginx $PUPPET_PARAMETER_INPUT[nginx_version]" > /etc/motd'
545
- end
546
- end
547
-
548
- context 'with existing puppet parameter with matching override' do
549
- let(:puppet_class) do
550
- puppetclass = FactoryBot.create(:puppetclass, :environments => [environment], :name => 'nginx')
551
- puppetclass.update_attribute(:hosts, [renderer.host])
552
- puppetclass
553
- end
554
- let(:lookup_key) do
555
- FactoryBot.create(:puppetclass_lookup_key, :as_smart_class_param,
556
- :key => 'version',
557
- :puppetclass => puppet_class,
558
- :path => 'fqdn',
559
- :override => true,
560
- :overrides => {"fqdn=#{renderer.host.fqdn}" => '1.4.7'})
561
- end
562
-
563
- describe 'rendering' do
564
- it 'renders the value from puppet parameter' do
565
- lookup_key
566
- _(result).must_equal 'echo "This is WebServer with nginx 1.4.7" > /etc/motd'
567
- end
568
- end
569
-
570
- describe 'preview' do
571
- it 'should render preview' do
572
- lookup_key
573
- _(renderer.preview).must_equal 'echo "This is WebServer with nginx 1.4.7" > /etc/motd'
574
- end
575
- end
576
- end
577
- end
578
-
579
- describe 'preview' do
580
- it 'should render preview' do
581
- _(renderer.preview).must_equal 'echo "This is WebServer with nginx $PUPPET_PARAMETER_INPUT[nginx_version]" > /etc/motd'
582
- end
583
- end
584
- end
585
- end
586
- end
587
499
  end