foreman-tasks 0.8.0 → 0.8.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.
- checksums.yaml +4 -4
- data/app/lib/actions/helpers/with_continuous_output.rb +26 -0
- data/app/lib/actions/helpers/with_delegated_action.rb +57 -0
- data/app/lib/actions/proxy_action.rb +27 -9
- data/app/services/foreman_tasks/proxy_selector.rb +58 -0
- data/foreman-tasks-core.gemspec +20 -0
- data/foreman-tasks.gemspec +1 -0
- data/lib/foreman_tasks.rb +1 -1
- data/lib/foreman_tasks/engine.rb +17 -4
- data/lib/foreman_tasks/tasks/cleanup.rake +1 -1
- data/lib/foreman_tasks/version.rb +1 -1
- data/lib/foreman_tasks_core.rb +19 -0
- data/lib/foreman_tasks_core/continuous_output.rb +50 -0
- data/lib/foreman_tasks_core/runner.rb +9 -0
- data/lib/foreman_tasks_core/runner/action.rb +69 -0
- data/lib/foreman_tasks_core/runner/base.rb +60 -0
- data/lib/foreman_tasks_core/runner/command_runner.rb +41 -0
- data/lib/foreman_tasks_core/runner/dispatcher.rb +158 -0
- data/lib/foreman_tasks_core/runner/update.rb +21 -0
- data/lib/foreman_tasks_core/settings_loader.rb +53 -0
- data/lib/foreman_tasks_core/shareable_action.rb +19 -0
- data/lib/foreman_tasks_core/version.rb +3 -0
- data/test/controllers/api/tasks_controller_test.rb +4 -1
- data/test/helpers/foreman_tasks/tasks_helper_test.rb +0 -2
- data/test/support/dummy_proxy_action.rb +6 -4
- data/test/unit/actions/proxy_action_test.rb +16 -7
- data/test/unit/proxy_selector_test.rb +59 -0
- metadata +33 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e52c285f1d8c6c98cc334d7d1028b190cebca6ea
|
4
|
+
data.tar.gz: 3d924a2162dfa1085a7d22c470184c0b5cebed61
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f8f42b585c90c6a2b5af2f57022a0bc8bd8c87367007f7dede5ed39c37431cdc84d553de1c3fb0e1876d34b667f31445e9f6afa191b85346e7ac92cad1ac4e21
|
7
|
+
data.tar.gz: 4397ef5b4c91c7598eb67edd6591341d44b725a955b2d048505e06c34a02d7ca3c670949fe7e196c6aa5963e667ef41fdf18a7ca71bdcda5af9baddb76ee7bc4
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Actions
|
2
|
+
module Helpers
|
3
|
+
module WithContinuousOutput
|
4
|
+
# @override
|
5
|
+
# array of objects defining fill_continuous_input
|
6
|
+
def continuous_output_providers
|
7
|
+
[]
|
8
|
+
end
|
9
|
+
|
10
|
+
def continuous_output
|
11
|
+
continuous_output = ::ForemanTasksCore::ContinuousOutput.new
|
12
|
+
continuous_output_providers.each do |continous_output_provider|
|
13
|
+
continous_output_provider.fill_continuous_output(continuous_output)
|
14
|
+
end
|
15
|
+
continuous_output
|
16
|
+
end
|
17
|
+
|
18
|
+
def fill_planning_errors_to_continuous_output(continuous_output)
|
19
|
+
execution_plan.errors.map do |e|
|
20
|
+
continuous_output.add_exception(_('Failed to initialize'), e, task.started_at)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Actions
|
2
|
+
module Helpers
|
3
|
+
module WithDelegatedAction
|
4
|
+
include ::Actions::Helpers::WithContinuousOutput
|
5
|
+
|
6
|
+
def plan_delegated_action(proxy, klass, options)
|
7
|
+
case proxy
|
8
|
+
when :not_defined
|
9
|
+
if klass.is_a?(String)
|
10
|
+
raise ::Foreman::Exception.new(_('No proxy defined for execution'))
|
11
|
+
else
|
12
|
+
delegated_action = plan_action(klass, options)
|
13
|
+
end
|
14
|
+
when :not_available
|
15
|
+
raise ::Foreman::Exception.new(_('All proxies with the required feature are unavailable at the moment'))
|
16
|
+
when ::SmartProxy
|
17
|
+
delegated_action = plan_action(::Actions::ProxyAction, proxy, klass, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
input[:delegated_action_id] = delegated_action.id
|
21
|
+
delegated_action
|
22
|
+
end
|
23
|
+
|
24
|
+
def humanized_output
|
25
|
+
delegated_output
|
26
|
+
end
|
27
|
+
|
28
|
+
def continuous_output_providers
|
29
|
+
super.tap do |ret|
|
30
|
+
ret << delegated_action if delegated_action.respond_to?(:fill_continuous_output)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def delegated_output
|
35
|
+
return @delegated_output if @delegated_output
|
36
|
+
action = delegated_action
|
37
|
+
@delegated_output = case action
|
38
|
+
when NilClass
|
39
|
+
{}
|
40
|
+
when ::Actions::ProxyAction
|
41
|
+
action.proxy_output(true)
|
42
|
+
else
|
43
|
+
action.output
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delegated_action
|
48
|
+
# TODO: make it easier in dynflow to load action data
|
49
|
+
delegated_step = task.execution_plan.steps.values.find_all do |step|
|
50
|
+
step.action_id == input[:delegated_action_id]
|
51
|
+
end.last
|
52
|
+
return unless delegated_step
|
53
|
+
world.persistence.load_action(delegated_step)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Actions
|
2
2
|
class ProxyAction < Base
|
3
|
-
|
4
3
|
include ::Dynflow::Action::Cancellable
|
5
4
|
include ::Dynflow::Action::Timeouts
|
6
5
|
|
@@ -12,10 +11,10 @@ module Actions
|
|
12
11
|
end
|
13
12
|
end
|
14
13
|
|
15
|
-
def plan(proxy, options)
|
14
|
+
def plan(proxy, klass, options)
|
16
15
|
options[:connection_options] ||= {}
|
17
16
|
default_connection_options.each { |key, value| options[:connection_options][key] ||= value }
|
18
|
-
plan_self(options.merge(:proxy_url => proxy.url))
|
17
|
+
plan_self(options.merge(:proxy_url => proxy.url, :proxy_action_name => klass.to_s))
|
19
18
|
end
|
20
19
|
|
21
20
|
def run(event = nil)
|
@@ -91,15 +90,31 @@ module Actions
|
|
91
90
|
|
92
91
|
# @override String name of an action to be triggered on server
|
93
92
|
def proxy_action_name
|
94
|
-
|
93
|
+
input[:proxy_action_name]
|
95
94
|
end
|
96
95
|
|
97
96
|
def proxy
|
98
97
|
ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => input[:proxy_url])
|
99
98
|
end
|
100
99
|
|
101
|
-
def proxy_output
|
102
|
-
output
|
100
|
+
def proxy_output(live = false)
|
101
|
+
if output.key?(:proxy_output)
|
102
|
+
output.fetch(:proxy_output) || {}
|
103
|
+
elsif live && output[:proxy_task_id]
|
104
|
+
proxy_data = proxy.status_of_task(output[:proxy_task_id])['actions'].detect { |action| action['class'] == proxy_action_name }
|
105
|
+
proxy_data.fetch('output', {})
|
106
|
+
else
|
107
|
+
{}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# The proxy action is able to contribute to continuous output
|
112
|
+
def fill_continuous_output(continuous_output)
|
113
|
+
failed_proxy_tasks.each do |failure_data|
|
114
|
+
message = _('Initialization error: %s') %
|
115
|
+
"#{failure_data[:exception_class]} - #{failure_data[:exception_message]}"
|
116
|
+
continuous_output.add_output(message, 'debug', failure_data[:timestamp])
|
117
|
+
end
|
103
118
|
end
|
104
119
|
|
105
120
|
def proxy_output=(output)
|
@@ -136,6 +151,10 @@ module Actions
|
|
136
151
|
|
137
152
|
private
|
138
153
|
|
154
|
+
def failed_proxy_tasks
|
155
|
+
metadata[:failed_proxy_tasks] ||= []
|
156
|
+
end
|
157
|
+
|
139
158
|
def with_connection_error_handling(event = nil)
|
140
159
|
yield event
|
141
160
|
rescue ::RestClient::Exception, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e
|
@@ -154,11 +173,10 @@ module Actions
|
|
154
173
|
end
|
155
174
|
|
156
175
|
def handle_connection_exception(exception, event = nil)
|
157
|
-
metadata[:failed_proxy_tasks] ||= []
|
158
176
|
options = input[:connection_options]
|
159
|
-
|
177
|
+
failed_proxy_tasks << format_exception(exception)
|
160
178
|
output[:proxy_task_id] = nil
|
161
|
-
if
|
179
|
+
if failed_proxy_tasks.count < options[:retry_count]
|
162
180
|
suspend do |suspended_action|
|
163
181
|
@world.clock.ping suspended_action,
|
164
182
|
Time.now + options[:retry_interval],
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
class ProxySelector
|
3
|
+
|
4
|
+
attr_reader :offline
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@tasks = {}
|
8
|
+
@offline = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def strategies
|
12
|
+
[:subnet, :fallback, :global]
|
13
|
+
end
|
14
|
+
|
15
|
+
def available_proxies(*args)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def determine_proxy(*args)
|
20
|
+
available_proxies = self.available_proxies(*args)
|
21
|
+
return :not_defined if available_proxies.empty? || available_proxies.values.all?(&:empty?)
|
22
|
+
proxy = nil
|
23
|
+
|
24
|
+
strategies.each do |strategy|
|
25
|
+
next unless available_proxies[strategy].present?
|
26
|
+
proxy = select_by_jobs_count(available_proxies[strategy])
|
27
|
+
break if proxy
|
28
|
+
end
|
29
|
+
|
30
|
+
proxy || :not_available
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get the least loaded proxy from the given list of proxies
|
34
|
+
def select_by_jobs_count(proxies)
|
35
|
+
exclude = @tasks.keys + @offline
|
36
|
+
@tasks.merge!(get_counts(proxies - exclude))
|
37
|
+
next_proxy = @tasks.select { |proxy, _| proxies.include?(proxy) }.
|
38
|
+
min_by { |_, job_count| job_count }.try(:first)
|
39
|
+
@tasks[next_proxy] += 1 if next_proxy.present?
|
40
|
+
next_proxy
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def get_counts(proxies)
|
46
|
+
proxies.inject({}) do |result, proxy|
|
47
|
+
begin
|
48
|
+
proxy_api = ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => proxy.url)
|
49
|
+
result[proxy] = proxy_api.tasks_count('running')
|
50
|
+
rescue => e
|
51
|
+
@offline << proxy
|
52
|
+
Foreman::Logging.exception "Could not fetch task counts from #{proxy}, skipped.", e
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
# Maintain your gem's version:
|
5
|
+
require "foreman_tasks_core/version"
|
6
|
+
|
7
|
+
# Describe your gem and declare its dependencies:
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = "foreman-tasks-core"
|
10
|
+
s.version = ForemanTasksCore::VERSION
|
11
|
+
s.authors = ["Ivan Nečas"]
|
12
|
+
s.email = ["inecas@redhat.com"]
|
13
|
+
s.homepage = "https://github.com/theforeman/foreman-tasks"
|
14
|
+
s.summary = "Common code used both at Forman and Foreman proxy regarding tasks"
|
15
|
+
s.description = <<DESC
|
16
|
+
Common code used both at Forman and Foreman proxy regarding tasks
|
17
|
+
DESC
|
18
|
+
|
19
|
+
s.files = Dir['lib/foreman_tasks_core/**/*'] + ['lib/foreman_tasks_core.rb']
|
20
|
+
end
|
data/foreman-tasks.gemspec
CHANGED
@@ -26,6 +26,7 @@ DESC
|
|
26
26
|
s.test_files = `git ls-files test`.split("\n")
|
27
27
|
s.extra_rdoc_files = Dir['README*', 'LICENSE']
|
28
28
|
|
29
|
+
s.add_dependency "foreman-tasks-core"
|
29
30
|
s.add_dependency "dynflow", '~> 0.8.13'
|
30
31
|
s.add_dependency "sequel" # for Dynflow process persistence
|
31
32
|
s.add_dependency "sinatra" # for Dynflow web console
|
data/lib/foreman_tasks.rb
CHANGED
@@ -37,7 +37,7 @@ module ForemanTasks
|
|
37
37
|
|
38
38
|
def self.sync_task(action, *args, &block)
|
39
39
|
trigger_task(false, action, *args, &block).tap do |task|
|
40
|
-
raise TaskError.new(task) if task.execution_plan.error?
|
40
|
+
raise TaskError.new(task) if task.execution_plan.error? || task.execution_plan.result == :warning
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
data/lib/foreman_tasks/engine.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'foreman_tasks_core'
|
1
2
|
require 'fast_gettext'
|
2
3
|
require 'gettext_i18n_rails'
|
3
4
|
|
@@ -107,12 +108,24 @@ module ForemanTasks
|
|
107
108
|
end
|
108
109
|
end
|
109
110
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
111
|
+
initializer "foreman_tasks.require_dynflow", :before => "foreman_tasks.initialize_dynflow" do |app|
|
112
|
+
ForemanTasks.dynflow.require!
|
113
|
+
::ForemanTasks.dynflow.config.on_init do |world|
|
114
|
+
ForemanTasksCore.dynflow_setup(world)
|
114
115
|
end
|
116
|
+
end
|
115
117
|
|
118
|
+
initializer 'foreman_tasks.set_core_settings' do
|
119
|
+
ForemanTasksCore::SettingsLoader.settings_registry.keys.each do |settings_keys|
|
120
|
+
settings = settings_keys.inject({}) do |h, settings_key|
|
121
|
+
h.merge(SETTINGS[settings_key] || {})
|
122
|
+
end
|
123
|
+
ForemanTasksCore::SettingsLoader.setup_settings(settings_keys.first, settings)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# to enable async Foreman operations using Dynflow
|
128
|
+
if ENV['FOREMAN_TASKS_MONKEYS'] == 'true'
|
116
129
|
config.to_prepare do
|
117
130
|
::Api::V2::HostsController.send :include, ForemanTasks::Concerns::HostsControllerExtension
|
118
131
|
::PuppetclassesController.send :include, ForemanTasks::Concerns::EnvironmentsExtension
|
@@ -50,7 +50,7 @@ DESC
|
|
50
50
|
desc 'Show the current configuration for auto-cleanup'
|
51
51
|
task :config => 'environment' do
|
52
52
|
if ForemanTasks::Cleaner.cleanup_settings[:after]
|
53
|
-
puts _('The tasks will be deleted after %{after}') % ForemanTasks::Cleaner.cleanup_settings[:after]
|
53
|
+
puts _('The tasks will be deleted after %{after}') % { :after => ForemanTasks::Cleaner.cleanup_settings[:after] }
|
54
54
|
else
|
55
55
|
puts _('Global period for cleaning up tasks is not set')
|
56
56
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# The goal of ForemanTasksCore is to collect parts of foreman-tasks
|
2
|
+
# that can be shared by the Foreman server and Foreman proxy
|
3
|
+
|
4
|
+
require 'foreman_tasks_core/settings_loader'
|
5
|
+
|
6
|
+
module ForemanTasksCore
|
7
|
+
def self.dynflow_world
|
8
|
+
raise "Dynflow world not set. Call initialize first" unless @dynflow_world
|
9
|
+
@dynflow_world
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.dynflow_present?
|
13
|
+
defined? Dynflow
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.dynflow_setup(dynflow_world)
|
17
|
+
@dynflow_world = dynflow_world
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ForemanTasksCore
|
2
|
+
class ContinuousOutput
|
3
|
+
attr_accessor :raw_outputs
|
4
|
+
|
5
|
+
def initialize(raw_outputs = [])
|
6
|
+
@raw_outputs = []
|
7
|
+
raw_outputs.each { |raw_output| add_raw_output(raw_output) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_raw_output(raw_output)
|
11
|
+
missing_args = %w[output_type output timestamp] - raw_output.keys
|
12
|
+
unless missing_args.empty?
|
13
|
+
raise ArgumentError, "Missing args for raw output: #{missing_args.inspect}"
|
14
|
+
end
|
15
|
+
@raw_outputs << raw_output
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty?
|
19
|
+
@raw_outputs.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def last_timestamp
|
23
|
+
return if @raw_outputs.empty?
|
24
|
+
@raw_outputs.last.fetch('timestamp')
|
25
|
+
end
|
26
|
+
|
27
|
+
def sort!
|
28
|
+
@raw_outputs.sort_by! { |record| record['timestamp'].to_f }
|
29
|
+
end
|
30
|
+
|
31
|
+
def humanize
|
32
|
+
sort!
|
33
|
+
raw_outputs.map { |output| output['output'] }.join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_exception(context, exception, timestamp = Time.now.getlocal)
|
37
|
+
add_output(context + ": #{exception.class} - #{exception.message}", 'debug', timestamp)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_output(*args)
|
41
|
+
add_raw_output(self.class.format_output(*args))
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.format_output(message, type = 'debug', timestamp = Time.now.getlocal)
|
45
|
+
{ 'output_type' => type,
|
46
|
+
'output' => message,
|
47
|
+
'timestamp' => timestamp.to_f }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'foreman_tasks_core/shareable_action'
|
2
|
+
module ForemanTasksCore
|
3
|
+
module Runner
|
4
|
+
class Action < ::ForemanTasksCore::ShareableAction
|
5
|
+
include ::Dynflow::Action::Cancellable
|
6
|
+
|
7
|
+
def run(event = nil)
|
8
|
+
case event
|
9
|
+
when nil
|
10
|
+
init_run
|
11
|
+
when Runner::Update
|
12
|
+
process_update(event)
|
13
|
+
when ::Dynflow::Action::Cancellable::Cancel
|
14
|
+
kill_run
|
15
|
+
else
|
16
|
+
raise "Unexpected event #{event.inspect}"
|
17
|
+
end
|
18
|
+
rescue => e
|
19
|
+
action_logger.error(e)
|
20
|
+
process_update(Runner::Update.encode_exception("Proxy error", e))
|
21
|
+
end
|
22
|
+
|
23
|
+
def finalize
|
24
|
+
# To mark the task as a whole as failed
|
25
|
+
error! "Script execution failed" if failed_run?
|
26
|
+
end
|
27
|
+
|
28
|
+
def rescue_strategy_for_self
|
29
|
+
::Dynflow::Action::Rescue::Fail
|
30
|
+
end
|
31
|
+
|
32
|
+
def initiate_runner
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def init_run
|
37
|
+
output[:result] = []
|
38
|
+
output[:runner_id] = runner_dispatcher.start(suspended_action, initiate_runner)
|
39
|
+
suspend
|
40
|
+
end
|
41
|
+
|
42
|
+
def runner_dispatcher
|
43
|
+
Runner::Dispatcher.instance
|
44
|
+
end
|
45
|
+
|
46
|
+
def kill_run
|
47
|
+
runner_dispatcher.kill(output[:runner_id])
|
48
|
+
suspend
|
49
|
+
end
|
50
|
+
|
51
|
+
def finish_run(update)
|
52
|
+
output[:exit_status] = update.exit_status
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_update(update)
|
56
|
+
output[:result].concat(update.continuous_output.raw_outputs)
|
57
|
+
if update.exit_status
|
58
|
+
finish_run(update)
|
59
|
+
else
|
60
|
+
suspend
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def failed_run?
|
65
|
+
output[:exit_status] != 0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ForemanTasksCore
|
2
|
+
module Runner
|
3
|
+
# Runner is an object that is able to initiate some action and
|
4
|
+
# provide update data on refresh call.
|
5
|
+
class Base
|
6
|
+
attr_reader :id
|
7
|
+
attr_accessor :logger
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
@id = SecureRandom.uuid
|
11
|
+
@continuous_output = ::ForemanTasksCore::ContinuousOutput.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= Logger.new(STDERR)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_refresh
|
19
|
+
logger.debug("refreshing runner")
|
20
|
+
refresh
|
21
|
+
new_data = @continuous_output
|
22
|
+
@continuous_output = ForemanTasksCore::ContinuousOutput.new
|
23
|
+
if !new_data.empty? || @exit_status
|
24
|
+
return Runner::Update.new(new_data, @exit_status)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def refresh
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def kill
|
37
|
+
# Override when you can kill the runner in the middle
|
38
|
+
end
|
39
|
+
|
40
|
+
def close
|
41
|
+
# if cleanup is needed
|
42
|
+
end
|
43
|
+
|
44
|
+
def publish_data(data, type)
|
45
|
+
@continuous_output.add_output(data, type)
|
46
|
+
end
|
47
|
+
|
48
|
+
def publish_exception(context, exception, fatal = true)
|
49
|
+
logger.error("#{context} - #{exception.class} #{exception.message}:\n" + \
|
50
|
+
exception.backtrace.join("\n"))
|
51
|
+
@continuous_output.add_exception(context, exception)
|
52
|
+
publish_exit_status('EXCEPTION') if fatal
|
53
|
+
end
|
54
|
+
|
55
|
+
def publish_exit_status(status)
|
56
|
+
@exit_status = status
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'io/wait'
|
2
|
+
require 'pty'
|
3
|
+
|
4
|
+
module ForemanTasksCore
|
5
|
+
module Runner
|
6
|
+
class CommandRunner < Runner::Base
|
7
|
+
def initialize_command(*command)
|
8
|
+
@command_out, @command_in, @command_pid = PTY.spawn(*command)
|
9
|
+
end
|
10
|
+
|
11
|
+
def refresh
|
12
|
+
return if @command_out.nil?
|
13
|
+
ready_outputs, * = IO.select([@command_out], nil, nil, 0.1)
|
14
|
+
if ready_outputs
|
15
|
+
if @command_out.nread > 0
|
16
|
+
lines = @command_out.read_nonblock(@command_out.nread)
|
17
|
+
else
|
18
|
+
close_io
|
19
|
+
Process.wait(@command_pid)
|
20
|
+
publish_exit_status($?.exitstatus)
|
21
|
+
end
|
22
|
+
publish_data(lines, 'stdout') if lines && !lines.empty?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
close_io
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def close_io
|
33
|
+
@command_out.close if @command_out && !@command_out.closed?
|
34
|
+
@command_out = nil
|
35
|
+
|
36
|
+
@command_in.close if @command_in && !@command_in.closed?
|
37
|
+
@command_in = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module ForemanTasksCore
|
2
|
+
module Runner
|
3
|
+
class Dispatcher
|
4
|
+
def self.instance
|
5
|
+
return @instance if @instance
|
6
|
+
@instance = self.new(ForemanTasksCore.dynflow_world.clock,
|
7
|
+
ForemanTasksCore.dynflow_world.logger)
|
8
|
+
end
|
9
|
+
|
10
|
+
class RunnerActor < ::Dynflow::Actor
|
11
|
+
def initialize(dispatcher, suspended_action, runner, clock, logger, options = {})
|
12
|
+
@dispatcher = dispatcher
|
13
|
+
@clock = clock
|
14
|
+
@logger = logger
|
15
|
+
@suspended_action = suspended_action
|
16
|
+
@runner = runner
|
17
|
+
@finishing = false
|
18
|
+
@refresh_interval = options[:refresh_interval] || 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_envelope(*args)
|
22
|
+
super
|
23
|
+
rescue => e
|
24
|
+
handle_exception(e)
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_runner
|
28
|
+
@logger.debug("start runner #{@runner.id}")
|
29
|
+
@runner.start
|
30
|
+
refresh_runner
|
31
|
+
ensure
|
32
|
+
plan_next_refresh
|
33
|
+
end
|
34
|
+
|
35
|
+
def refresh_runner
|
36
|
+
@logger.debug("refresh runner #{@runner.id}")
|
37
|
+
if update = @runner.run_refresh
|
38
|
+
@suspended_action << update
|
39
|
+
finish if update.exit_status
|
40
|
+
end
|
41
|
+
ensure
|
42
|
+
@refresh_planned = false
|
43
|
+
plan_next_refresh
|
44
|
+
end
|
45
|
+
|
46
|
+
def kill
|
47
|
+
@logger.debug("kill runner #{@runner.id}")
|
48
|
+
@runner.kill
|
49
|
+
rescue => e
|
50
|
+
handle_exception(e, false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def finish
|
54
|
+
@logger.debug("finish runner #{@runner.id}")
|
55
|
+
@finishing = true
|
56
|
+
@dispatcher.finish(@runner.id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def start_termination(*args)
|
60
|
+
@logger.debug("terminate #{@runner.id}")
|
61
|
+
super
|
62
|
+
@runner.close
|
63
|
+
finish_termination
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def plan_next_refresh
|
69
|
+
if !@finishing && !@refresh_planned
|
70
|
+
@logger.debug("planning to refresh #{@runner.id}")
|
71
|
+
@clock.ping(reference, Time.now + @refresh_interval, :refresh_runner)
|
72
|
+
@refresh_planned = true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_exception(exception, fatal = true)
|
77
|
+
@dispatcher.handle_command_exception(@runner.id, exception, fatal)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize(clock, logger)
|
82
|
+
@mutex = Mutex.new
|
83
|
+
@clock = clock
|
84
|
+
@logger = logger
|
85
|
+
@runner_actors = {}
|
86
|
+
@runner_suspended_actions = {}
|
87
|
+
end
|
88
|
+
|
89
|
+
def synchronize(&block)
|
90
|
+
@mutex.synchronize(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def start(suspended_action, runner)
|
94
|
+
synchronize do
|
95
|
+
begin
|
96
|
+
raise "Actor with runner id #{runner.id} already exists" if @runner_actors[runner.id]
|
97
|
+
runner.logger = @logger
|
98
|
+
runner_actor = RunnerActor.spawn("runner-actor-#{runner.id}", self, suspended_action, runner, @clock, @logger)
|
99
|
+
@runner_actors[runner.id] = runner_actor
|
100
|
+
@runner_suspended_actions[runner.id] = suspended_action
|
101
|
+
runner_actor.tell(:start_runner)
|
102
|
+
return runner.id
|
103
|
+
rescue => exception
|
104
|
+
_handle_command_exception(runner.id, exception)
|
105
|
+
return nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def kill(runner_id)
|
111
|
+
synchronize do
|
112
|
+
begin
|
113
|
+
runner_actor = @runner_actors[runner_id]
|
114
|
+
runner_actor.tell(:kill) if runner_actor
|
115
|
+
rescue => exception
|
116
|
+
_handle_command_exception(runner_id, exception, false)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def finish(runner_id)
|
122
|
+
synchronize do
|
123
|
+
begin
|
124
|
+
_finish(runner_id)
|
125
|
+
rescue => exception
|
126
|
+
_handle_command_exception(runner_id, exception, false)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_command_exception(*args)
|
132
|
+
synchronize { _handle_command_exception(*args) }
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def _finish(runner_id)
|
138
|
+
runner_actor = @runner_actors.delete(runner_id)
|
139
|
+
return unless runner_actor
|
140
|
+
@logger.debug("closing session for command [#{runner_id}]," +
|
141
|
+
"#{@runner_actors.size} actors left ")
|
142
|
+
runner_actor.tell([:start_termination, Concurrent.future])
|
143
|
+
ensure
|
144
|
+
@runner_suspended_actions.delete(runner_id)
|
145
|
+
end
|
146
|
+
|
147
|
+
def _handle_command_exception(runner_id, exception, fatal = true)
|
148
|
+
@logger.error("error while dispatching request to runner #{runner_id}:"\
|
149
|
+
"#{exception.class} #{exception.message}:\n #{exception.backtrace.join("\n")}")
|
150
|
+
suspended_action = @runner_suspended_actions[runner_id]
|
151
|
+
if suspended_action
|
152
|
+
suspended_action << Runner::Update.encode_exception("Runner error", exception, fatal)
|
153
|
+
end
|
154
|
+
_finish(runner_id) if fatal
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'foreman_tasks_core/continuous_output'
|
2
|
+
|
3
|
+
module ForemanTasksCore
|
4
|
+
module Runner
|
5
|
+
# Runner::Update represents chunk of data produced by runner that
|
6
|
+
# can be consumed by other components, such as RunnerAction
|
7
|
+
class Update
|
8
|
+
attr_reader :continuous_output, :exit_status
|
9
|
+
def initialize(continuous_output, exit_status)
|
10
|
+
@continuous_output = continuous_output
|
11
|
+
@exit_status = exit_status
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.encode_exception(context, exception, fatal = true)
|
15
|
+
continuous_output = ::ForemanTasksCore::ContinuousOutput.new
|
16
|
+
continuous_output.add_exception(context, exception)
|
17
|
+
return self.new(continuous_output, fatal ? 'EXCEPTION' : nil)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ForemanTasksCore
|
2
|
+
module SettingsLoader
|
3
|
+
def self.settings_registry
|
4
|
+
@settings_registry ||= {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.name_to_settings
|
8
|
+
@name_to_settings ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.settings_keys
|
12
|
+
@settings_keys ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.settings_registered?(name)
|
16
|
+
name_to_settings.key?(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.register_settings(names, object)
|
20
|
+
names = [names] unless names.is_a? Array
|
21
|
+
names.each do |name|
|
22
|
+
raise "settings name has to be a symbol" unless name.is_a? Symbol
|
23
|
+
raise "settings #{name} already registered" if SettingsLoader.settings_registered?(name)
|
24
|
+
name_to_settings[name] = object
|
25
|
+
end
|
26
|
+
settings_registry[names] = object
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.setup_settings(name, settings)
|
30
|
+
raise "Settings for #{name} were not registered" unless settings_registered?(name)
|
31
|
+
name_to_settings[name].initialize_settings(settings)
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_settings(names, defaults = {})
|
35
|
+
SettingsLoader.register_settings(names, self)
|
36
|
+
@defaults = defaults
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize_settings(settings = {})
|
40
|
+
@settings = @defaults.merge(settings)
|
41
|
+
validate_settings!
|
42
|
+
end
|
43
|
+
|
44
|
+
def settings
|
45
|
+
raise "Settings for #{self} not initalized" unless @settings
|
46
|
+
@settings
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_settings!
|
50
|
+
raise "Only symbols expected in keys" unless @settings.keys.all? { |key| key.is_a? Symbol }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ForemanTasksCore
|
2
|
+
class ShareableAction < ::Dynflow::Action
|
3
|
+
def plan(input)
|
4
|
+
input = input.dup
|
5
|
+
callback = input.delete('callback')
|
6
|
+
if callback
|
7
|
+
input[:task_id] = callback['task_id']
|
8
|
+
else
|
9
|
+
input[:task_id] ||= SecureRandom.uuid
|
10
|
+
end
|
11
|
+
|
12
|
+
planned_action = plan_self(input)
|
13
|
+
# code only applicable, when run with SmartProxyDynflowCore in place
|
14
|
+
if defined?(SmartProxyDynflowCore::Callback) && callback
|
15
|
+
plan_action(SmartProxyDynflowCore::Callback::Action, callback, planned_action.output)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -26,7 +26,10 @@ module ForemanTasks
|
|
26
26
|
it 'passes the data to the corresponding action' do
|
27
27
|
Support::DummyProxyAction.reset
|
28
28
|
|
29
|
-
triggered = ForemanTasks.trigger(Support::DummyProxyAction,
|
29
|
+
triggered = ForemanTasks.trigger(Support::DummyProxyAction,
|
30
|
+
Support::DummyProxyAction.proxy,
|
31
|
+
'Proxy::DummyAction',
|
32
|
+
'foo' => 'bar')
|
30
33
|
Support::DummyProxyAction.proxy.task_triggered.wait(5)
|
31
34
|
|
32
35
|
task = ForemanTasks::Task.where(:external_id => triggered.id).first
|
@@ -24,12 +24,14 @@ module Support
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
class ProxySelector < ::ForemanTasks::ProxySelector
|
28
|
+
def available_proxies
|
29
|
+
{ :global => [DummyProxyAction.proxy] }
|
30
|
+
end
|
29
31
|
end
|
30
32
|
|
31
|
-
def
|
32
|
-
|
33
|
+
def proxy
|
34
|
+
self.class.proxy
|
33
35
|
end
|
34
36
|
|
35
37
|
def task
|
@@ -8,18 +8,24 @@ module ForemanTasks
|
|
8
8
|
|
9
9
|
before do
|
10
10
|
Support::DummyProxyAction.reset
|
11
|
-
@action = create_and_plan_action(Support::DummyProxyAction,
|
11
|
+
@action = create_and_plan_action(Support::DummyProxyAction,
|
12
|
+
Support::DummyProxyAction.proxy,
|
13
|
+
'Proxy::DummyAction',
|
14
|
+
'foo' => 'bar')
|
12
15
|
@action = run_action(@action)
|
13
16
|
end
|
14
17
|
|
15
18
|
describe 'first run' do
|
16
19
|
it 'triggers the corresponding action on the proxy' do
|
17
20
|
proxy_call = Support::DummyProxyAction.proxy.log[:trigger_task].first
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
expected_call = ['Proxy::DummyAction',
|
22
|
+
{ 'foo' => 'bar',
|
23
|
+
'connection_options' =>
|
24
|
+
{ 'retry_interval' => 15, 'retry_count' => 4, 'timeout' => 60 },
|
25
|
+
'proxy_url' => 'proxy.example.com',
|
26
|
+
'proxy_action_name'=>'Proxy::DummyAction',
|
27
|
+
'callback' => { 'task_id' => '123', 'step_id' => @action.run_step_id } }]
|
28
|
+
proxy_call.must_equal(expected_call)
|
23
29
|
end
|
24
30
|
end
|
25
31
|
|
@@ -64,7 +70,10 @@ module ForemanTasks
|
|
64
70
|
end
|
65
71
|
|
66
72
|
it 'handles connection errors' do
|
67
|
-
action = create_and_plan_action(Support::DummyProxyAction,
|
73
|
+
action = create_and_plan_action(Support::DummyProxyAction,
|
74
|
+
Support::DummyProxyAction.proxy,
|
75
|
+
'Proxy::DummyAction',
|
76
|
+
{ :foo => 'bar' })
|
68
77
|
run_stubbed_action = lambda do |lambda_action|
|
69
78
|
run_action lambda_action do |block_action|
|
70
79
|
block_action.expects(:trigger_proxy_task).raises(Errno::ECONNREFUSED.new('Connection refused'))
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "foreman_tasks_test_helper"
|
2
|
+
|
3
|
+
describe ForemanTasks::ProxySelector do
|
4
|
+
let(:proxy_selector) { ForemanTasks::ProxySelector.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.stubs(:tasks_count).returns(0)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#select_by_jobs_count' do
|
11
|
+
it 'load balances' do
|
12
|
+
count = 3
|
13
|
+
ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.expects(:tasks_count).raises.
|
14
|
+
then.times(count - 1).returns(0)
|
15
|
+
proxies = FactoryGirl.create_list(:smart_proxy, count)
|
16
|
+
|
17
|
+
available = proxies.reduce([]) do |found, _|
|
18
|
+
found << proxy_selector.select_by_jobs_count(proxies)
|
19
|
+
end
|
20
|
+
|
21
|
+
available.count.must_equal count
|
22
|
+
available.uniq.count.must_equal count - 1
|
23
|
+
proxy_selector.offline.count.must_equal 1
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns nil for if no proxy is available' do
|
27
|
+
proxy_selector.select_by_jobs_count([]).must_be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#determine_proxy' do
|
32
|
+
it 'returns :not_defined when avialable proxies returns empty hash' do
|
33
|
+
proxy_selector.stubs(:available_proxies => [])
|
34
|
+
proxy_selector.determine_proxy.must_equal :not_defined
|
35
|
+
proxy_selector.stubs(:available_proxies => { :global => [] })
|
36
|
+
proxy_selector.determine_proxy.must_equal :not_defined
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns :not_available when proxies are set but offline' do
|
40
|
+
count = 3
|
41
|
+
ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.expects(:tasks_count).times(count).raises
|
42
|
+
proxy_selector.stubs(:available_proxies =>
|
43
|
+
{ :global => FactoryGirl.create_list(:smart_proxy, count) })
|
44
|
+
proxy_selector.determine_proxy.must_equal :not_available
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns first available proxy, prioritizing by strategy' do
|
48
|
+
ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.expects(:tasks_count).returns(0)
|
49
|
+
fallback_proxy = FactoryGirl.build(:smart_proxy)
|
50
|
+
global_proxy = FactoryGirl.build(:smart_proxy)
|
51
|
+
ForemanTasks::ProxySelector.any_instance.stubs(:available_proxies =>
|
52
|
+
{ :fallback => [fallback_proxy],
|
53
|
+
:global => [global_proxy] })
|
54
|
+
ForemanTasks::ProxySelector.new.determine_proxy.must_equal fallback_proxy
|
55
|
+
ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.expects(:tasks_count).raises.then.returns(0)
|
56
|
+
ForemanTasks::ProxySelector.new.determine_proxy.must_equal global_proxy
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman-tasks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: foreman-tasks-core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: dynflow
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +136,8 @@ files:
|
|
122
136
|
- app/lib/actions/helpers/args_serialization.rb
|
123
137
|
- app/lib/actions/helpers/humanizer.rb
|
124
138
|
- app/lib/actions/helpers/lock.rb
|
139
|
+
- app/lib/actions/helpers/with_continuous_output.rb
|
140
|
+
- app/lib/actions/helpers/with_delegated_action.rb
|
125
141
|
- app/lib/actions/middleware/inherit_task_groups.rb
|
126
142
|
- app/lib/actions/middleware/keep_current_user.rb
|
127
143
|
- app/lib/actions/middleware/recurring_logic.rb
|
@@ -144,6 +160,7 @@ files:
|
|
144
160
|
- app/models/foreman_tasks/task_groups/recurring_logic_task_group.rb
|
145
161
|
- app/models/foreman_tasks/triggering.rb
|
146
162
|
- app/models/setting/foreman_tasks.rb
|
163
|
+
- app/services/foreman_tasks/proxy_selector.rb
|
147
164
|
- app/views/common/_trigger_form.html.erb
|
148
165
|
- app/views/foreman_tasks/api/recurring_logics/base.json.rabl
|
149
166
|
- app/views/foreman_tasks/api/recurring_logics/index.json.rabl
|
@@ -187,6 +204,7 @@ files:
|
|
187
204
|
- deploy/foreman-tasks.service
|
188
205
|
- deploy/foreman-tasks.sysconfig
|
189
206
|
- extra/dynflow-executor.example
|
207
|
+
- foreman-tasks-core.gemspec
|
190
208
|
- foreman-tasks.gemspec
|
191
209
|
- lib/foreman-tasks.rb
|
192
210
|
- lib/foreman_tasks.rb
|
@@ -205,6 +223,17 @@ files:
|
|
205
223
|
- lib/foreman_tasks/test_extensions.rb
|
206
224
|
- lib/foreman_tasks/triggers.rb
|
207
225
|
- lib/foreman_tasks/version.rb
|
226
|
+
- lib/foreman_tasks_core.rb
|
227
|
+
- lib/foreman_tasks_core/continuous_output.rb
|
228
|
+
- lib/foreman_tasks_core/runner.rb
|
229
|
+
- lib/foreman_tasks_core/runner/action.rb
|
230
|
+
- lib/foreman_tasks_core/runner/base.rb
|
231
|
+
- lib/foreman_tasks_core/runner/command_runner.rb
|
232
|
+
- lib/foreman_tasks_core/runner/dispatcher.rb
|
233
|
+
- lib/foreman_tasks_core/runner/update.rb
|
234
|
+
- lib/foreman_tasks_core/settings_loader.rb
|
235
|
+
- lib/foreman_tasks_core/shareable_action.rb
|
236
|
+
- lib/foreman_tasks_core/version.rb
|
208
237
|
- lib/tasks/gettext.rake
|
209
238
|
- locale/Makefile
|
210
239
|
- locale/action_names.rb
|
@@ -224,6 +253,7 @@ files:
|
|
224
253
|
- test/unit/actions/proxy_action_test.rb
|
225
254
|
- test/unit/cleaner_test.rb
|
226
255
|
- test/unit/dynflow_console_authorizer_test.rb
|
256
|
+
- test/unit/proxy_selector_test.rb
|
227
257
|
- test/unit/recurring_logic_test.rb
|
228
258
|
- test/unit/task_groups_test.rb
|
229
259
|
- test/unit/task_test.rb
|
@@ -265,6 +295,7 @@ test_files:
|
|
265
295
|
- test/unit/actions/proxy_action_test.rb
|
266
296
|
- test/unit/cleaner_test.rb
|
267
297
|
- test/unit/dynflow_console_authorizer_test.rb
|
298
|
+
- test/unit/proxy_selector_test.rb
|
268
299
|
- test/unit/recurring_logic_test.rb
|
269
300
|
- test/unit/task_groups_test.rb
|
270
301
|
- test/unit/task_test.rb
|