foreman-tasks 0.8.0 → 0.8.1

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