foreman-tasks 0.15.0 → 0.15.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.babelrc +24 -0
  3. data/.eslintrc +27 -0
  4. data/.gitignore +3 -0
  5. data/.prettierrc +4 -0
  6. data/.rubocop.yml +1 -1
  7. data/.storybook/addons.js +2 -0
  8. data/.storybook/config.js +7 -0
  9. data/.storybook/webpack.config.js +66 -0
  10. data/.stylelintrc +5 -0
  11. data/.travis.yml +5 -0
  12. data/.yo-rc.json +5 -0
  13. data/README.md +1 -0
  14. data/app/controllers/foreman_tasks/api/tasks_controller.rb +28 -10
  15. data/app/controllers/foreman_tasks/react_controller.rb +17 -0
  16. data/app/lib/actions/middleware/proxy_batch_triggering.rb +36 -0
  17. data/app/lib/actions/middleware/watch_delegated_proxy_sub_tasks.rb +12 -7
  18. data/app/lib/actions/proxy_action.rb +52 -16
  19. data/app/lib/proxy_api/foreman_dynflow/dynflow_proxy.rb +12 -0
  20. data/app/models/foreman_tasks/remote_task.rb +74 -0
  21. data/app/models/foreman_tasks/task/dynflow_task.rb +14 -7
  22. data/app/models/setting/foreman_tasks.rb +3 -1
  23. data/app/views/foreman_tasks/layouts/react.html.erb +12 -0
  24. data/app/views/foreman_tasks/tasks/show.html.erb +1 -1
  25. data/config/routes.rb +3 -0
  26. data/db/migrate/20181019135324_add_remote_task_operation.rb +5 -0
  27. data/foreman-tasks.gemspec +1 -1
  28. data/lib/foreman_tasks/engine.rb +1 -0
  29. data/lib/foreman_tasks/version.rb +1 -1
  30. data/lib/foreman_tasks.rb +5 -1
  31. data/package.json +117 -0
  32. data/script/travis_run_js_tests.sh +7 -0
  33. data/test/controllers/api/tasks_controller_test.rb +3 -0
  34. data/test/core/unit/dispatcher_test.rb +43 -0
  35. data/test/core/unit/runner_test.rb +129 -0
  36. data/test/core/unit/task_launcher_test.rb +56 -0
  37. data/test/foreman_tasks_core_test_helper.rb +4 -0
  38. data/test/support/dummy_proxy_action.rb +17 -1
  39. data/test/unit/actions/proxy_action_test.rb +20 -2
  40. data/test/unit/actions/recurring_action_test.rb +1 -1
  41. data/test/unit/remote_task_test.rb +41 -0
  42. data/test/unit/task_test.rb +3 -1
  43. data/webpack/ForemanTasks/ForemanTasks.js +27 -0
  44. data/webpack/ForemanTasks/ForemanTasks.test.js +10 -0
  45. data/webpack/ForemanTasks/Routes/ForemanTasksRouter.js +14 -0
  46. data/webpack/ForemanTasks/Routes/ForemanTasksRouter.test.js +22 -0
  47. data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.js +17 -0
  48. data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.test.js +16 -0
  49. data/webpack/ForemanTasks/Routes/IndexTasks/IndexTasks.js +10 -0
  50. data/webpack/ForemanTasks/Routes/IndexTasks/__tests__/IndexTasks.test.js +10 -0
  51. data/webpack/ForemanTasks/Routes/IndexTasks/__tests__/__snapshots__/IndexTasks.test.js.snap +12 -0
  52. data/webpack/ForemanTasks/Routes/IndexTasks/index.js +1 -0
  53. data/webpack/ForemanTasks/Routes/IndexTasks/indexTasks.scss +0 -0
  54. data/webpack/ForemanTasks/Routes/ShowTask/ShowTask.js +10 -0
  55. data/webpack/ForemanTasks/Routes/ShowTask/__tests__/ShowTask.test.js +10 -0
  56. data/webpack/ForemanTasks/Routes/ShowTask/__tests__/__snapshots__/ShowTask.test.js.snap +12 -0
  57. data/webpack/ForemanTasks/Routes/ShowTask/index.js +1 -0
  58. data/webpack/ForemanTasks/Routes/ShowTask/showTask.scss +0 -0
  59. data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRouter.test.js.snap +16 -0
  60. data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRoutes.test.js.snap +21 -0
  61. data/webpack/ForemanTasks/__snapshots__/ForemanTasks.test.js.snap +60 -0
  62. data/webpack/ForemanTasks/components/Hello/Hello.stories.js +5 -0
  63. data/webpack/ForemanTasks/components/Hello/__tests__/Hello.test.js +11 -0
  64. data/webpack/ForemanTasks/components/Hello/__tests__/__snapshots__/Hello.test.js.snap +7 -0
  65. data/webpack/ForemanTasks/components/Hello/index.js +5 -0
  66. data/webpack/ForemanTasks/index.js +1 -0
  67. data/webpack/index.js +11 -0
  68. data/webpack/stories/index.js +12 -0
  69. data/webpack/test_setup.js +6 -0
  70. metadata +56 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e29d69e4d1a7ad804db75f4ce19d20004bdebced6e85e04d2648bcb5c571c2b1
4
- data.tar.gz: 50e6b2708a35731579806eaef2a13c83dee4c92244db7a1a8f73c712e565891a
3
+ metadata.gz: d9be0418b3db898832481c46ba3f3eae300e2562d7fa240d71cb96876ee825de
4
+ data.tar.gz: 61db2a1487d48b7312478c3ad65adffa2230c2fbe9f5a452d200cc4b56428962
5
5
  SHA512:
6
- metadata.gz: 311b48cc5a9cd2d8a502635f70a544b8e73fae9f7495148d67976f5e2ae0615c96ef7ca184b6992923e885abe942f0354e3f6ce0c76d0449f65099c1aede8077
7
- data.tar.gz: f92ebcdad66e0f02ea0b3f27a954520792fe2e31a0057639fcec59e68658be686db75086c936aae6811b5ded4307cb9effccdec95e0554d7d7b047f6fb7d5762
6
+ metadata.gz: d303c00a15e97cbf2c5fe443559a5bd2260298e8b54e52e552040c93fb8885caba6130f7957ce097bfbddafee4a4b3b1bd1a8d41786471b88cd306876496f2f9
7
+ data.tar.gz: f4a036829b034d94577d0affce68fdb899bbcdeaf0bef74f8b2358d0cb165ac9e42b7afc8111060210d6eb90bc5289d3af5d49ca6fa88b4f05bd7464a5c94480
data/.babelrc ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "presets": [
3
+ "env",
4
+ "react"
5
+ ],
6
+ "plugins": [
7
+ ["module-resolver", {
8
+ "alias": {
9
+ "root": ["./"],
10
+ "foremanReact": "../foreman/webpack/assets/javascripts/react_app"
11
+ }
12
+ }],
13
+ "transform-class-properties",
14
+ "transform-object-rest-spread",
15
+ "transform-object-assign",
16
+ "lodash",
17
+ "syntax-dynamic-import"
18
+ ],
19
+ "env": {
20
+ "test": {
21
+ "plugins": ["dynamic-import-node"]
22
+ }
23
+ }
24
+ }
data/.eslintrc ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "plugins": ["patternfly-react"],
3
+ "extends": ["plugin:patternfly-react/recommended"],
4
+ "rules": {
5
+ "prettier/prettier": ["error", {
6
+ "singleQuote": true,
7
+ "trailingComma": "es5"
8
+ }]
9
+ },
10
+ "settings": {
11
+ "import/resolver": {
12
+ "babel-module": {}
13
+ }
14
+ },
15
+ "globals": {
16
+ "document": false,
17
+ "escape": false,
18
+ "navigator": false,
19
+ "unescape": false,
20
+ "window": false,
21
+ "$": true,
22
+ "_": true,
23
+ "__": true,
24
+ "n__": true,
25
+ "d3": true
26
+ }
27
+ }
data/.gitignore CHANGED
@@ -13,3 +13,6 @@ locale/*.mo
13
13
  locale/*/*.pox
14
14
  locale/*/*.edit.po
15
15
  locale/*/*.po.time_stamp
16
+ node_modules
17
+ package-lock.json
18
+ coverage/
data/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "es5"
4
+ }
data/.rubocop.yml CHANGED
@@ -59,7 +59,7 @@ Rails/ReversibleMigration:
59
59
  Metrics/BlockLength:
60
60
  Exclude:
61
61
  - config/routes.rb
62
- - lib/foreman_remote_execution/engine.rb
62
+ - lib/foreman_tasks/engine.rb
63
63
  - test/**/*
64
64
  - lib/foreman_tasks/tasks/**/*
65
65
 
@@ -0,0 +1,2 @@
1
+ import '@storybook/addon-actions/register';
2
+ import '@storybook/addon-knobs/register';
@@ -0,0 +1,7 @@
1
+ import { configure } from '@storybook/react';
2
+
3
+ function loadStories() {
4
+ require('../webpack/stories');
5
+ }
6
+
7
+ configure(loadStories, module);
@@ -0,0 +1,66 @@
1
+ let path = require('path');
2
+
3
+ // Use storybook's default configuration with our customizations
4
+ module.exports = (baseConfig, env, defaultConfig) => {
5
+
6
+ // overwrite storybook's default import rules
7
+ defaultConfig.module.rules = [
8
+ {
9
+ test: /\.js$/,
10
+ exclude: /node_modules/,
11
+ loader: 'babel-loader',
12
+ options: {
13
+ presets: [
14
+ path.join(__dirname, '..', 'node_modules/babel-preset-react'),
15
+ path.join(__dirname, '..', 'node_modules/babel-preset-env')
16
+ ],
17
+ plugins: [
18
+ path.join(__dirname, '..', 'node_modules/babel-plugin-transform-class-properties'),
19
+ path.join(__dirname, '..', 'node_modules/babel-plugin-transform-object-rest-spread'),
20
+ path.join(__dirname, '..', 'node_modules/babel-plugin-transform-object-assign'),
21
+ path.join(__dirname, '..', 'node_modules/babel-plugin-syntax-dynamic-import')
22
+ ]
23
+ }
24
+ },
25
+ {
26
+ test: /(\.png|\.gif)$/,
27
+ loader: 'url-loader?limit=32767'
28
+ },
29
+ {
30
+ test: /\.css$/,
31
+ loaders: ['style-loader', 'css-loader']
32
+ },
33
+ {
34
+ test: /\.scss$/,
35
+ loaders: ['style-loader', 'css-loader', {
36
+ loader: 'sass-loader',
37
+ options: {
38
+ includePaths: [
39
+ // teach webpack to resolve patternfly dependencies
40
+ path.resolve(__dirname, '..', 'node_modules', 'patternfly', 'dist', 'sass'),
41
+ path.resolve(__dirname, '..', 'node_modules', 'bootstrap-sass', 'assets', 'stylesheets'),
42
+ path.resolve(__dirname, '..', 'node_modules', 'font-awesome-sass', 'assets', 'stylesheets')
43
+ ]
44
+ }
45
+ }]
46
+ },
47
+ {
48
+ test: /\.md$/,
49
+ loaders: ['raw-loader']
50
+ },
51
+ {
52
+ test: /(\.ttf|\.woff|\.woff2|\.eot|\.svg|\.jpg)$/,
53
+ loaders: ['url-loader']
54
+ },
55
+ ]
56
+
57
+ defaultConfig.resolve = {
58
+ modules: [
59
+ path.join(__dirname, '..', 'webpack'),
60
+ path.join(__dirname, '..', 'node_modules'),
61
+ 'node_modules/',
62
+ ],
63
+ };
64
+
65
+ return defaultConfig;
66
+ };
data/.stylelintrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "stylelint-config-standard",
4
+ ],
5
+ }
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: node_js
2
+ node_js:
3
+ - '8' # current LTS
4
+ - '10' # future LTS
5
+ script: ./script/travis_run_js_tests.sh
data/.yo-rc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "generator-react-domain": {
3
+ "depsInstalled": true
4
+ }
5
+ }
data/README.md CHANGED
@@ -22,6 +22,7 @@ happening/happened in your Foreman instance. A framework for asynchronous tasks
22
22
  | >= 1.17 | ~> 0.11.0 |
23
23
  | >= 1.18 | ~> 0.13.0 |
24
24
  | >= 1.20 | ~> 0.14.0 |
25
+ | >= 1.22 | ~> 0.15.0 |
25
26
 
26
27
  Installation
27
28
  ------------
@@ -149,23 +149,41 @@ module ForemanTasks
149
149
  }
150
150
  end
151
151
 
152
- api :POST, '/tasks/callback', N_('Send data to the task from external executor (such as smart_proxy_dynflow)')
153
- param :callback, Hash do
154
- param :task_id, :identifier, :desc => N_('UUID of the task')
155
- param :step_id, String, :desc => N_('The ID of the step inside the execution plan to send the event to')
152
+ def_param_group :callback_target do
153
+ param :callback, Hash do
154
+ param :task_id, :identifier, :desc => N_('UUID of the task')
155
+ param :step_id, String, :desc => N_('The ID of the step inside the execution plan to send the event to')
156
+ end
156
157
  end
157
- param :data, Hash, :desc => N_('Data to be sent to the action')
158
+
159
+ def_param_group :callback do
160
+ param_group :callback_target
161
+ param :data, Hash, :desc => N_('Data to be sent to the action')
162
+ end
163
+
164
+ api :POST, '/tasks/callback', N_('Send data to the task from external executor (such as smart_proxy_dynflow)')
165
+ param_group :callback
166
+ param :callbacks, Array, :of => param_group(:callback)
158
167
  def callback
159
- task = ForemanTasks::Task::DynflowTask.find(params[:callback][:task_id])
160
- ForemanTasks.dynflow.world.event(task.external_id,
161
- params[:callback][:step_id].to_i,
162
- # We need to call .to_unsafe_h to unwrap the hash from ActionController::Parameters
163
- ::Actions::ProxyAction::CallbackData.new(params[:data].to_unsafe_h, :request_id => ::Logging.mdc['request']))
168
+ callbacks = params.key?(:callback) ? Array(params) : params[:callbacks]
169
+ ids = callbacks.map { |payload| payload[:callback][:task_id] }
170
+ external_map = Hash[*ForemanTasks::Task.where(:id => ids).pluck(:id, :external_id).flatten]
171
+ callbacks.each do |payload|
172
+ # We need to call .to_unsafe_h to unwrap the hash from ActionController::Parameters
173
+ callback = payload[:callback]
174
+ process_callback(external_map[callback[:task_id]], callback[:step_id].to_i, payload[:data].to_unsafe_h, :request_id => ::Logging.mdc['request'])
175
+ end
164
176
  render :json => { :message => 'processing' }.to_json
165
177
  end
166
178
 
167
179
  private
168
180
 
181
+ def process_callback(execution_plan_uuid, step_id, data, meta)
182
+ ForemanTasks.dynflow.world.event(execution_plan_uuid,
183
+ step_id,
184
+ ::Actions::ProxyAction::CallbackData.new(data, meta))
185
+ end
186
+
169
187
  def search_tasks(search_params)
170
188
  scope = resource_scope_for_index.select('DISTINCT foreman_tasks_tasks.*')
171
189
  scope = ordering_scope(scope, search_params)
@@ -0,0 +1,17 @@
1
+ module ForemanTasks
2
+ class ReactController < ::ApplicationController
3
+ def index
4
+ render 'foreman_tasks/layouts/react', :layout => false
5
+ end
6
+
7
+ private
8
+
9
+ def controller_permission
10
+ :foreman_tasks
11
+ end
12
+
13
+ def action_permission
14
+ :view
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module Actions
2
+ module Middleware
3
+ class ProxyBatchTriggering < ::Dynflow::Middleware
4
+ # If the event could result into sub tasks being planned, check if there are any RemoteTasks
5
+ # to trigger after the event is processed
6
+ #
7
+ # The ProxyAction needs to be planned with `:use_batch_triggering => true` to activate the feature
8
+ def run(event = nil)
9
+ pass event
10
+ ensure
11
+ trigger_remote_tasks if event.nil? || event.is_a?(Dynflow::Action::WithBulkSubPlans::PlanNextBatch)
12
+ end
13
+
14
+ def trigger_remote_tasks
15
+ # Find the tasks in batches, order them by proxy_url so we get all tasks
16
+ # to a certain proxy "close to each other"
17
+ remote_tasks.pending.order(:proxy_url, :id).find_in_batches(:batch_size => batch_size) do |batch|
18
+ # Group the tasks by operation, in theory there should be only one operation
19
+ batch.group_by(&:operation).each do |operation, group|
20
+ ForemanTasks::RemoteTask.batch_trigger(operation, group)
21
+ end
22
+ end
23
+ end
24
+
25
+ def remote_tasks
26
+ action.task.remote_sub_tasks
27
+ end
28
+
29
+ private
30
+
31
+ def batch_size
32
+ Setting['foreman_tasks_proxy_batch_size']
33
+ end
34
+ end
35
+ end
36
+ end
@@ -26,13 +26,11 @@ module Actions
26
26
  end
27
27
 
28
28
  def check_triggered
29
- # Sort by proxy_url so there are less requests per proxy
30
- source = remote_tasks.triggered.order(:proxy_url, :id)
31
- source.find_in_batches(:batch_size => BATCH_SIZE) do |batch|
32
- tasks = batch.group_by(&:proxy_url)
33
- .map { |(url, tasks)| poll_proxy_tasks(url, tasks) }
34
- .flatten
35
- process_task_results tasks
29
+ in_remote_task_batches(remote_tasks.triggered) do |batch|
30
+ batch.group_by(&:proxy_url).each do |(url, tasks)|
31
+ tasks = poll_proxy_tasks(url, tasks).flatten
32
+ process_task_results tasks
33
+ end
36
34
  end
37
35
  end
38
36
 
@@ -68,6 +66,13 @@ module Actions
68
66
  action.action_logger.warn(_('Failed to check on tasks on proxy at %{url}: %{exception}') % { :url => url, :exception => e.message })
69
67
  []
70
68
  end
69
+
70
+ def in_remote_task_batches(scope)
71
+ # Sort by proxy_url so there are less requests per proxy
72
+ scope.order(:proxy_url, :id).find_in_batches(:batch_size => BATCH_SIZE) do |batch|
73
+ yield batch
74
+ end
75
+ end
71
76
  end
72
77
  end
73
78
  end
@@ -26,8 +26,15 @@ module Actions
26
26
 
27
27
  def plan(proxy, klass, options)
28
28
  options[:connection_options] ||= {}
29
- default_connection_options.each { |key, value| options[:connection_options][key] ||= value }
30
- plan_self(options.merge(:proxy_url => proxy.url, :proxy_action_name => klass.to_s))
29
+ default_connection_options.each do |key, value|
30
+ options[:connection_options][key] = value unless options[:connection_options].key?(key)
31
+ end
32
+ plan_self(options.merge(:proxy_url => proxy.url, :proxy_action_name => klass.to_s, :proxy_version => proxy_version(proxy)))
33
+ # Just saving the RemoteTask is enough when using batch triggering
34
+ # It will be picked up by the ProxyBatchTriggering middleware
35
+ if input[:use_batch_triggering] && with_batch_triggering?(input[:proxy_version])
36
+ prepare_remote_task.save!
37
+ end
31
38
  end
32
39
 
33
40
  def run(event = nil)
@@ -64,20 +71,22 @@ module Actions
64
71
 
65
72
  def trigger_proxy_task
66
73
  suspend do |_suspended_action|
67
- response = proxy.trigger_task(proxy_action_name,
68
- input.merge(:callback => { :task_id => task.id,
69
- :step_id => run_step_id }))
70
- ::ForemanTasks::RemoteTask.new(:remote_task_id => response['task_id'], :execution_plan_id => execution_plan_id,
71
- :state => 'triggered', :proxy_url => input[:proxy_url], :step_id => run_step_id).save!
72
- output[:proxy_task_id] = response['task_id']
74
+ remote_task = prepare_remote_task
75
+ remote_task.trigger(proxy_action_name, proxy_input)
76
+ output[:proxy_task_id] = remote_task.remote_task_id
73
77
  end
74
78
  end
75
79
 
80
+ def proxy_input(task_id = task.id)
81
+ input.merge(:callback => { :task_id => task_id,
82
+ :step_id => run_step_id })
83
+ end
84
+
76
85
  def check_task_status
77
- response = proxy.status_of_task(output[:proxy_task_id])
86
+ response = proxy.status_of_task(proxy_task_id)
78
87
  if %w[stopped paused].include? response['state']
79
88
  if response['result'] == 'error'
80
- raise ::Foreman::Exception, _('The smart proxy task %s failed.') % output[:proxy_task_id]
89
+ raise ::Foreman::Exception, _('The smart proxy task %s failed.') % proxy_task_id
81
90
  else
82
91
  on_data(response['actions'].find { |block_action| block_action['class'] == proxy_action_name }['output'])
83
92
  end
@@ -90,14 +99,14 @@ module Actions
90
99
  if output[:cancel_sent]
91
100
  error! ForemanTasks::Task::TaskCancelledException.new(_('Cancel enforced: the task might be still running on the proxy'))
92
101
  else
93
- proxy.cancel_task(output[:proxy_task_id])
102
+ proxy.cancel_task(proxy_task_id)
94
103
  output[:cancel_sent] = true
95
104
  suspend
96
105
  end
97
106
  end
98
107
 
99
108
  def abort_proxy_task
100
- proxy.cancel_task(output[:proxy_task_id])
109
+ proxy.cancel_task(proxy_task_id)
101
110
  error! ForemanTasks::Task::TaskCancelledException.new(_('Task aborted: the task might be still running on the proxy'))
102
111
  end
103
112
 
@@ -132,6 +141,11 @@ module Actions
132
141
  input[:proxy_action_name]
133
142
  end
134
143
 
144
+ # @override String name of a operation to be triggered on server
145
+ def proxy_operation_name
146
+ input[:proxy_operation_name]
147
+ end
148
+
135
149
  def proxy
136
150
  ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => input[:proxy_url])
137
151
  end
@@ -139,8 +153,8 @@ module Actions
139
153
  def proxy_output(live = false)
140
154
  if output.key?(:proxy_output) || state == :error
141
155
  output.fetch(:proxy_output, {})
142
- elsif live && output[:proxy_task_id]
143
- proxy_data = proxy.status_of_task(output[:proxy_task_id])['actions'].detect { |action| action['class'] == proxy_action_name }
156
+ elsif live && proxy_task_id
157
+ proxy_data = proxy.status_of_task(proxy_task_id)['actions'].detect { |action| action['class'] == proxy_action_name }
144
158
  proxy_data.fetch('output', {})
145
159
  else
146
160
  {}
@@ -174,7 +188,13 @@ module Actions
174
188
  # Fails if the plan is not finished within 60 seconds from the first task trigger attempt on the smart proxy
175
189
  # If the triggering fails, it retries 3 more times with 15 second delays
176
190
  { :retry_interval => Setting['foreman_tasks_proxy_action_retry_interval'] || 15,
177
- :retry_count => Setting['foreman_tasks_proxy_action_retry_count'] || 4 }
191
+ :retry_count => Setting['foreman_tasks_proxy_action_retry_count'] || 4,
192
+ :proxy_batch_triggering => Setting['foreman_tasks_proxy_batch_trigger'] || false }
193
+ end
194
+
195
+ def with_batch_triggering?(proxy_version)
196
+ (proxy_version[:major] == 1 && proxy_version[:minor] > 20) || proxy_version[:major] > 1 &&
197
+ input.fetch(:connection_options, {}).fetch(:proxy_batch_triggering, false)
178
198
  end
179
199
 
180
200
  def clean_remote_task(*_args)
@@ -183,6 +203,11 @@ module Actions
183
203
 
184
204
  private
185
205
 
206
+ def proxy_version(proxy)
207
+ match = proxy.statuses[:version].version['version'].match(/(\d+)\.(\d+)\.(\d+)/)
208
+ { :major => match[1].to_i, :minor => match[2].to_i, :patch => match[3].to_i }
209
+ end
210
+
186
211
  def failed_proxy_tasks
187
212
  metadata[:failed_proxy_tasks] ||= []
188
213
  end
@@ -198,7 +223,7 @@ module Actions
198
223
  end
199
224
 
200
225
  def format_exception(exception)
201
- { :proxy_task_id => output[:proxy_task_id],
226
+ { :proxy_task_id => proxy_task_id,
202
227
  :exception_class => exception.class.name,
203
228
  :exception_message => exception.message,
204
229
  :timestamp => Time.now.to_f }
@@ -218,5 +243,16 @@ module Actions
218
243
  raise exception
219
244
  end
220
245
  end
246
+
247
+ def prepare_remote_task
248
+ ::ForemanTasks::RemoteTask.new(:execution_plan_id => execution_plan_id,
249
+ :proxy_url => input[:proxy_url],
250
+ :step_id => run_step_id,
251
+ :operation => proxy_operation_name)
252
+ end
253
+
254
+ def proxy_task_id
255
+ output[:proxy_task_id] ||= remote_task.try(:remote_task_id)
256
+ end
221
257
  end
222
258
  end
@@ -39,6 +39,18 @@ module ProxyAPI
39
39
  payload = MultiJson.dump(:task_ids => ids)
40
40
  MultiJson.load(Task.new(@args).send(:post, payload, 'status'))
41
41
  end
42
+
43
+ def operations
44
+ MultiJson.load(Task.new(@args).send(:get, 'operations'))
45
+ end
46
+
47
+ def launch_tasks(operation, input, options = {})
48
+ data = { :input => input,
49
+ :operation => operation,
50
+ :options => options }
51
+ payload = MultiJson.dump(data)
52
+ MultiJson.load(Task.new(@args).send(:post, payload, 'launch'))
53
+ end
42
54
  end
43
55
  end
44
56
  end
@@ -8,5 +8,79 @@ module ForemanTasks
8
8
  :inverse_of => :remote_tasks
9
9
 
10
10
  scope :triggered, -> { where(:state => 'triggered') }
11
+ scope :pending, -> { where(:state => 'new') }
12
+
13
+ delegate :proxy_action_name, :to => :action
14
+
15
+ # Triggers a task on the proxy "the old way"
16
+ def trigger(proxy_action_name, input)
17
+ response = begin
18
+ proxy.trigger_task(proxy_action_name, input).merge('result' => 'success')
19
+ rescue RestClient::Exception => e
20
+ logger.warn "Could not trigger task on the smart proxy: #{e.message}"
21
+ {}
22
+ end
23
+ update_from_batch_trigger(response)
24
+ save!
25
+ end
26
+
27
+ def self.batch_trigger(operation, remote_tasks)
28
+ remote_tasks.group_by(&:proxy_url).values.map do |group|
29
+ input_hash = group.reduce({}) do |acc, remote_task|
30
+ acc.merge(remote_task.execution_plan_id => { :action_input => remote_task.proxy_input,
31
+ :action_class => remote_task.proxy_action_name })
32
+ end
33
+ safe_batch_trigger(operation, group, input_hash)
34
+ end
35
+ remote_tasks
36
+ end
37
+
38
+ # Attempt to trigger the tasks using the new API and fall back to the old one
39
+ # if it fails
40
+ def self.safe_batch_trigger(operation, remote_tasks, input_hash)
41
+ results = remote_tasks.first.proxy.launch_tasks(operation, input_hash)
42
+ remote_tasks.each { |remote_task| remote_task.update_from_batch_trigger results[remote_task.execution_plan_id] }
43
+ rescue RestClient::NotFound
44
+ fallback_batch_trigger remote_tasks, input_hash
45
+ end
46
+
47
+ # Trigger the tasks one-by-one using the old API
48
+ def self.fallback_batch_trigger(remote_tasks, input_hash)
49
+ remote_tasks.each do |remote_task|
50
+ task_data = input_hash[remote_task.execution_plan_id]
51
+ remote_task.trigger(task_data[:action_class], task_data[:action_input])
52
+ end
53
+ end
54
+
55
+ def update_from_batch_trigger(data)
56
+ if data['result'] == 'success'
57
+ self.remote_task_id = data['task_id']
58
+ self.state = 'triggered'
59
+ else
60
+ # Tell the action the task on the smart proxy stopped
61
+ ForemanTasks.dynflow.world.event execution_plan_id,
62
+ step_id,
63
+ ::Actions::ProxyAction::ProxyActionStopped.new
64
+ end
65
+ save!
66
+ end
67
+
68
+ def proxy_input
69
+ action.proxy_input(task.id)
70
+ end
71
+
72
+ def proxy
73
+ @proxy ||= ::ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => proxy_url)
74
+ end
75
+
76
+ private
77
+
78
+ def action
79
+ @action ||= ForemanTasks.dynflow.world.persistence.load_action(step)
80
+ end
81
+
82
+ def step
83
+ @step ||= task.execution_plan.steps[step_id]
84
+ end
11
85
  end
12
86
  end
@@ -83,7 +83,7 @@ module ForemanTasks
83
83
  end
84
84
 
85
85
  def input
86
- return execution_plan_action.input['job_arguments'] if active_job?
86
+ return active_job_data['arguments'] if active_job?
87
87
  main_action.task_input if main_action.respond_to?(:task_input)
88
88
  end
89
89
 
@@ -113,12 +113,8 @@ module ForemanTasks
113
113
  def main_action
114
114
  return @main_action if defined?(@main_action)
115
115
  if active_job?
116
- args = if execution_plan.delay_record
117
- execution_plan.delay_record.args.first
118
- else
119
- execution_plan_action.input
120
- end
121
- @main_action = active_job_action(args['job_class'], args['job_arguments'])
116
+ job_data = active_job_data
117
+ @main_action = active_job_action(job_data['job_class'], job_data['arguments'])
122
118
  else
123
119
  @main_action = execution_plan && execution_plan.root_plan_step.try(:action, execution_plan)
124
120
  end
@@ -140,6 +136,17 @@ module ForemanTasks
140
136
  execution_plan_action.class == ::Dynflow::ActiveJob::QueueAdapters::JobWrapper
141
137
  end
142
138
 
139
+ def active_job_data
140
+ args = if execution_plan.delay_record
141
+ execution_plan.delay_record.args.first
142
+ else
143
+ execution_plan_action.input
144
+ end
145
+ return args['job_data'] if args.key?('job_data')
146
+ # For dynflow <= 1.2.1
147
+ { 'job_class' => args['job_class'], 'arguments' => args['job_arguments'] }
148
+ end
149
+
143
150
  def execution_plan_action
144
151
  execution_plan.root_plan_step.action(execution_plan)
145
152
  end
@@ -10,7 +10,9 @@ class Setting::ForemanTasks < Setting
10
10
  set('dynflow_enable_console', N_('Enable the dynflow console (/foreman_tasks/dynflow) for debugging'), true),
11
11
  set('dynflow_console_require_auth', N_('Require user to be authenticated as user with admin rights when accessing dynflow console'), true),
12
12
  set('foreman_tasks_proxy_action_retry_count', N_('Number of attempts to start a task on the smart proxy before failing'), 4),
13
- set('foreman_tasks_proxy_action_retry_interval', N_('Time in seconds between retries'), 15)
13
+ set('foreman_tasks_proxy_action_retry_interval', N_('Time in seconds between retries'), 15),
14
+ set('foreman_tasks_proxy_batch_trigger', N_('Allow triggering tasks on the smart proxy in batches'), true),
15
+ set('foreman_tasks_proxy_batch_size', N_('Number of tasks which should be sent to the smart proxy in one request, if foreman_tasks_proxy_batch_trigger is enabled'), 1000)
14
16
  ].each { |s| create! s.update(:category => 'Setting::ForemanTasks') }
15
17
  end
16
18
 
@@ -0,0 +1,12 @@
1
+ <% content_for(:javascripts) do %>
2
+ <%= webpacked_plugins_js_for :'foreman-tasks' %>
3
+ <% end %>
4
+
5
+ <% content_for(:content) do %>
6
+ <%= notifications %>
7
+ <div id="organization-id" data-id="<%= Organization.current.id if Organization.current %>" ></div>
8
+ <div id="user-id" data-id="<%= User.current.id if User.current %>" ></div>
9
+ <div id="foremanTasksReactRoot"></div>
10
+ <% end %>
11
+ <%= render file: "layouts/base" %>
12
+ <%= mount_react_component('ForemanTasks', '#foremanTasksReactRoot') %>
@@ -7,7 +7,7 @@
7
7
  url: url_for(foreman_tasks_tasks_path)
8
8
  },
9
9
  {
10
- caption: truncate(format_task_input(@task), :length => 50)
10
+ caption: format_task_input(@task)
11
11
  }
12
12
  ],
13
13
  name_field: 'action',