foreman-tasks-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0a8b2eaeb4519b3538cd8d76919d5ce6d0adb43a
4
+ data.tar.gz: 51c3e5a7c71129c8104f6fc753c8db0d090fc2d8
5
+ SHA512:
6
+ metadata.gz: 5214dcc03b6e1a2be518bb29eac25e6158adc6e90b21798622ff1f5e2e3a8aa6c74907a769566e4564669ee846c66996ff2803e7f3055beb229c2946d5f7d663
7
+ data.tar.gz: 0ad9eac594955242b38937ebd6e4217dfa447f26bda5f81c456fa2dec4e6e3500f19b0cc1834a82555b18caa7976ceb0e032662142491f25d71793c95264f074
@@ -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,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,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
@@ -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
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foreman-tasks-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ivan Nečas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Common code used both at Forman and Foreman proxy regarding tasks
15
+ email:
16
+ - inecas@redhat.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/foreman_tasks_core.rb
22
+ - lib/foreman_tasks_core/continuous_output.rb
23
+ - lib/foreman_tasks_core/runner.rb
24
+ - lib/foreman_tasks_core/runner/action.rb
25
+ - lib/foreman_tasks_core/runner/base.rb
26
+ - lib/foreman_tasks_core/runner/command_runner.rb
27
+ - lib/foreman_tasks_core/runner/dispatcher.rb
28
+ - lib/foreman_tasks_core/runner/update.rb
29
+ - lib/foreman_tasks_core/settings_loader.rb
30
+ - lib/foreman_tasks_core/shareable_action.rb
31
+ - lib/foreman_tasks_core/version.rb
32
+ homepage: https://github.com/theforeman/foreman-tasks
33
+ licenses: []
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.4.5
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Common code used both at Forman and Foreman proxy regarding tasks
55
+ test_files: []