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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9309d18f9110a2295d933c2464788a8cceb42161
4
- data.tar.gz: 94bc5fd42d4c176312d80ca1a45733593a23a395
3
+ metadata.gz: e52c285f1d8c6c98cc334d7d1028b190cebca6ea
4
+ data.tar.gz: 3d924a2162dfa1085a7d22c470184c0b5cebed61
5
5
  SHA512:
6
- metadata.gz: 7536f877ffa338b8195477b8c2e14ca9856a0b3f125941ad1fda48383cf5c883c9061f3934e6b3ec68de7ffb706d69faa08709edc88c821139ac76765e3e5c48
7
- data.tar.gz: f6285300dd2b63c27705c6a2fdc7f0e263e1f52faf62a4d0d40e41fa2566367df994daae4eaaebece8635b8ca0e62d9a07ab7643c04117bcc4915acbc8bfbf9a
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
- raise NotImplemented
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[:proxy_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
- metadata[:failed_proxy_tasks] << format_exception(exception)
177
+ failed_proxy_tasks << format_exception(exception)
160
178
  output[:proxy_task_id] = nil
161
- if metadata[:failed_proxy_tasks].count < options[:retry_count]
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
@@ -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
 
@@ -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
- # to enable async Foreman operations using Dynflow
111
- if ENV['FOREMAN_TASKS_MONKEYS'] == 'true'
112
- initializer "foreman_tasks.require_dynflow", :before => "foreman_tasks.initialize_dynflow" do |app|
113
- ForemanTasks.dynflow.require!
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
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.1"
3
3
  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,9 @@
1
+ module ForemanTasksCore
2
+ module Runner
3
+ end
4
+ end
5
+
6
+ require 'foreman_tasks_core/runner/update'
7
+ require 'foreman_tasks_core/runner/base'
8
+ require 'foreman_tasks_core/runner/dispatcher'
9
+ require 'foreman_tasks_core/runner/action'
@@ -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
@@ -0,0 +1,3 @@
1
+ module ForemanTasksCore
2
+ VERSION = '0.1.0'
3
+ 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, Support::DummyProxyAction.proxy, 'foo' => 'bar')
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
@@ -40,7 +40,5 @@ module ForemanTasks
40
40
  format_task_input(@task, true).must_equal("Create #{response}")
41
41
  end
42
42
  end
43
-
44
- end
45
43
  end
46
44
  end
@@ -24,12 +24,14 @@ module Support
24
24
  end
25
25
  end
26
26
 
27
- def proxy
28
- self.class.proxy
27
+ class ProxySelector < ::ForemanTasks::ProxySelector
28
+ def available_proxies
29
+ { :global => [DummyProxyAction.proxy] }
30
+ end
29
31
  end
30
32
 
31
- def proxy_action_name
32
- 'Proxy::DummyAction'
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, Support::DummyProxyAction.proxy, 'foo' => 'bar')
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
- proxy_call.must_equal(["Proxy::DummyAction",
19
- { "foo" => "bar",
20
- "connection_options" => { "retry_interval" => 15, "retry_count" => 4, "timeout" => 60 },
21
- "proxy_url" => "proxy.example.com",
22
- "callback" => { "task_id" => "123", "step_id" => @action.run_step_id }}])
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, Support::DummyProxyAction.proxy, { :foo => 'bar' })
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.0
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-08-12 00:00:00.000000000 Z
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