foreman-tasks 0.15.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
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',