foreman-tasks-core 0.1.0

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 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: []