foreman-tasks 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.
- data/MIT-LICENSE +20 -0
- data/README.md +139 -0
- data/app/controllers/foreman_tasks/api/tasks_controller.rb +140 -0
- data/app/controllers/foreman_tasks/concerns/hosts_controller_extension.rb +26 -0
- data/app/controllers/foreman_tasks/tasks_controller.rb +19 -0
- data/app/helpers/foreman_tasks/tasks_helper.rb +16 -0
- data/app/lib/actions/base.rb +36 -0
- data/app/lib/actions/entry_action.rb +51 -0
- data/app/lib/actions/foreman/architecture/create.rb +29 -0
- data/app/lib/actions/foreman/architecture/destroy.rb +28 -0
- data/app/lib/actions/foreman/architecture/update.rb +21 -0
- data/app/lib/actions/foreman/host/import_facts.rb +40 -0
- data/app/lib/actions/helpers/args_serialization.rb +91 -0
- data/app/lib/actions/helpers/humanizer.rb +64 -0
- data/app/lib/actions/helpers/lock.rb +43 -0
- data/app/lib/actions/test_action.rb +17 -0
- data/app/models/foreman_tasks/concerns/action_subject.rb +102 -0
- data/app/models/foreman_tasks/concerns/architecture_action_subject.rb +20 -0
- data/app/models/foreman_tasks/concerns/host_action_subject.rb +42 -0
- data/app/models/foreman_tasks/lock.rb +176 -0
- data/app/models/foreman_tasks/task.rb +86 -0
- data/app/models/foreman_tasks/task/dynflow_task.rb +65 -0
- data/app/views/foreman_tasks/api/tasks/show.json.rabl +5 -0
- data/app/views/foreman_tasks/tasks/index.html.erb +51 -0
- data/app/views/foreman_tasks/tasks/show.html.erb +77 -0
- data/bin/dynflow-executor +43 -0
- data/config/routes.rb +20 -0
- data/db/migrate/20131205204140_create_foreman_tasks.rb +15 -0
- data/db/migrate/20131209122644_create_foreman_tasks_locks.rb +12 -0
- data/lib/foreman-tasks.rb +1 -0
- data/lib/foreman_tasks.rb +20 -0
- data/lib/foreman_tasks/dynflow.rb +101 -0
- data/lib/foreman_tasks/dynflow/configuration.rb +86 -0
- data/lib/foreman_tasks/dynflow/daemon.rb +88 -0
- data/lib/foreman_tasks/dynflow/persistence.rb +36 -0
- data/lib/foreman_tasks/engine.rb +58 -0
- data/lib/foreman_tasks/tasks/dynflow.rake +7 -0
- data/lib/foreman_tasks/version.rb +3 -0
- data/test/tasks_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +196 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
<div class="task-details">
|
2
|
+
<%= form_for @task, :url => "#" do %>
|
3
|
+
<div>
|
4
|
+
<span class="param-name"><%= _("Id") %>:</span>
|
5
|
+
<span class="param-value"><%= @task.id %></span>
|
6
|
+
</div>
|
7
|
+
<div>
|
8
|
+
<span class="param-name"><%= _("Label") %>:</span>
|
9
|
+
<span class="param-value"><%= @task.label %></span>
|
10
|
+
</div>
|
11
|
+
<div>
|
12
|
+
<span class="param-name"><%= _("Name") %>:</span>
|
13
|
+
<span class="param-value"><%= @task.humanized[:action] %></span>
|
14
|
+
</div>
|
15
|
+
<div>
|
16
|
+
<span class="param-name"><%= _("Owner") %>:</span>
|
17
|
+
<span class="param-value"><%= @task.username %></span>
|
18
|
+
</div>
|
19
|
+
<div>
|
20
|
+
<span class="param-name"><%= _("Started at") %>:</span>
|
21
|
+
<span class="param-value"><%= @task.started_at %></span>
|
22
|
+
</div>
|
23
|
+
<div>
|
24
|
+
<span class="param-name"><%= _("Ended at") %>:</span>
|
25
|
+
<span class="param-value"><%= @task.ended_at %></span>
|
26
|
+
</div>
|
27
|
+
<div>
|
28
|
+
<span class="param-name"><%= _("State") %>:</span>
|
29
|
+
<span class="param-value"><%= @task.state %></span>
|
30
|
+
</div>
|
31
|
+
<div>
|
32
|
+
<span class="param-name"><%= _("Result") %>:</span>
|
33
|
+
<span class="param-value"><%= @task.result %></span>
|
34
|
+
</div>
|
35
|
+
<div>
|
36
|
+
<span class="param-name"><%= _("Params") %>:</span>
|
37
|
+
<span class="param-value"><%= format_task_input(@task) %></span>
|
38
|
+
</div>
|
39
|
+
<div>
|
40
|
+
<span class="param-name"><%= _("Progress") %>:</span>
|
41
|
+
<span class="param-value">
|
42
|
+
<div class="progress progress-striped">
|
43
|
+
<div class="bar" style="width: <%= 100 * @task.progress %>%;"></div>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
<% if @task.cli_example %>
|
47
|
+
<div>
|
48
|
+
<span class="param-name"><%= _("CLI Example") %>:</span>
|
49
|
+
<span class="param-value">
|
50
|
+
<pre><%= @task.cli_example %></pre>
|
51
|
+
</span>
|
52
|
+
</div>
|
53
|
+
<% end %>
|
54
|
+
<div>
|
55
|
+
<span class="param-name"><%= _("Output") %>:</span>
|
56
|
+
<span class="param-value">
|
57
|
+
<pre><%= @task.humanized[:output] %></pre>
|
58
|
+
</span>
|
59
|
+
</div>
|
60
|
+
<div>
|
61
|
+
<span class="param-name"><%= _("Raw input") %>:</span>
|
62
|
+
<span class="param-value">
|
63
|
+
<pre><%= @task.input.pretty_inspect %></pre>
|
64
|
+
</span>
|
65
|
+
</div>
|
66
|
+
<div>
|
67
|
+
<span class="param-name"><%= _("Raw output") %>:</span>
|
68
|
+
<span class="param-value">
|
69
|
+
<pre><%= @task.output.pretty_inspect %></pre>
|
70
|
+
</span>
|
71
|
+
</div>
|
72
|
+
<div>
|
73
|
+
<span class="param-name"><%= _("External Id") %>:</span>
|
74
|
+
<span class="param-value"><%= @task.external_id %></span>
|
75
|
+
</div>
|
76
|
+
<% end %>
|
77
|
+
</div>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
options = { foreman_root: Dir.pwd }
|
6
|
+
|
7
|
+
opts = OptionParser.new do |opts|
|
8
|
+
opts.banner = <<BANNER
|
9
|
+
Run Dynflow executor for Foreman tasks.
|
10
|
+
|
11
|
+
Usage: #{File.basename($0)} [options] ACTION"
|
12
|
+
|
13
|
+
ACTION can be one of:
|
14
|
+
|
15
|
+
* start - start the executor on background. It creates these files
|
16
|
+
in tmp/pid directory:
|
17
|
+
|
18
|
+
* dynflow_executor_monitor.pid - pid of monitor ensuring
|
19
|
+
the executor keeps running
|
20
|
+
* dynflow_executor.pid - pid of the executor itself
|
21
|
+
* dynflow_executor.output - stdout of the executor
|
22
|
+
* stop - stops the running executor
|
23
|
+
* restart - restarts the running executor
|
24
|
+
* run - run the executor in foreground
|
25
|
+
|
26
|
+
BANNER
|
27
|
+
|
28
|
+
opts.on('-h', '--help', 'Show this message') do
|
29
|
+
puts opts
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
opts.on('-f', '--foreman-root=PATH', "Path to Foreman Rails root path. By default '#{options[:foreman_root]}'") do |path|
|
33
|
+
options[:foreman_root] = path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
args = opts.parse!(ARGV)
|
38
|
+
command = args.first || 'run'
|
39
|
+
|
40
|
+
app_file = File.expand_path('./config/application', options[:foreman_root])
|
41
|
+
require app_file
|
42
|
+
|
43
|
+
ForemanTasks::Dynflow::Daemon.new.run_background(command, options)
|
data/config/routes.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Foreman::Application.routes.draw do
|
2
|
+
namespace :foreman_tasks do
|
3
|
+
resources :tasks, :only => [:index, :show] do
|
4
|
+
collection do
|
5
|
+
get 'auto_complete_search'
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
namespace :api do
|
10
|
+
resources :tasks, :only => [:show] do
|
11
|
+
post :bulk_search, :on => :collection
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if ForemanTasks.dynflow.required?
|
16
|
+
require 'dynflow/web_console'
|
17
|
+
mount ForemanTasks.dynflow.web_console => "/dynflow"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateForemanTasks < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :foreman_tasks_tasks, :id => false do |t|
|
4
|
+
t.string :id, primary_key: true
|
5
|
+
t.string :type, index: true, null: false
|
6
|
+
t.string :label, index: true
|
7
|
+
t.datetime :started_at, index: true
|
8
|
+
t.datetime :ended_at, index: true
|
9
|
+
t.string :state, index: true, null: false
|
10
|
+
t.string :result, index: true, null: false
|
11
|
+
t.decimal :progress, index: true, precision: 5, scale: 4
|
12
|
+
t.string :external_id, index: true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateForemanTasksLocks < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :foreman_tasks_locks do |t|
|
4
|
+
t.string :task_id, index: true, null: false
|
5
|
+
t.string :name, index: true, null: false
|
6
|
+
t.string :resource_type, index: true
|
7
|
+
t.integer :resource_id
|
8
|
+
t.boolean :exclusive, index: true
|
9
|
+
end
|
10
|
+
add_index :foreman_tasks_locks, [:resource_type, :resource_id]
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'foreman_tasks'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'foreman_tasks/version'
|
2
|
+
require 'foreman_tasks/engine'
|
3
|
+
require 'foreman_tasks/dynflow'
|
4
|
+
|
5
|
+
module ForemanTasks
|
6
|
+
|
7
|
+
def self.dynflow
|
8
|
+
@dynflow ||= ForemanTasks::Dynflow.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.trigger(action, *args, &block)
|
12
|
+
dynflow.world.trigger action, *args, &block
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.async_task(action, *args, &block)
|
16
|
+
run = trigger(action, *args, &block)
|
17
|
+
ForemanTasks::Task::DynflowTask.find_by_external_id(run.id)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'dynflow'
|
2
|
+
|
3
|
+
module ForemanTasks
|
4
|
+
# Class for configuring and preparing the Dynflow runtime environment.
|
5
|
+
class Dynflow
|
6
|
+
require 'foreman_tasks/dynflow/configuration'
|
7
|
+
require 'foreman_tasks/dynflow/persistence'
|
8
|
+
require 'foreman_tasks/dynflow/daemon'
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@required = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def config
|
15
|
+
@config ||= ForemanTasks::Dynflow::Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# call this method if your engine uses Dynflow
|
19
|
+
def require!
|
20
|
+
@required = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def required?
|
24
|
+
@required
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialized?
|
28
|
+
!@world.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize!
|
32
|
+
return unless @required
|
33
|
+
return @world if @world
|
34
|
+
config.initialize_world.tap do |world|
|
35
|
+
@world = world
|
36
|
+
|
37
|
+
ActionDispatch::Reloader.to_prepare do
|
38
|
+
ForemanTasks.dynflow.eager_load_actions!
|
39
|
+
world.reload!
|
40
|
+
end
|
41
|
+
|
42
|
+
unless config.remote?
|
43
|
+
at_exit { world.terminate.wait }
|
44
|
+
|
45
|
+
# for now, we can check the consistency only when there
|
46
|
+
# is no remote executor. We should be able to check the consistency
|
47
|
+
# every time the new world is created when there is a register
|
48
|
+
# of executors
|
49
|
+
world.consistency_check
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Mark that the process is executor. This prevents the remote setting from
|
55
|
+
# applying. Needs to be set up before the world is being initialized
|
56
|
+
def executor!
|
57
|
+
@executor = true
|
58
|
+
end
|
59
|
+
|
60
|
+
def executor?
|
61
|
+
@executor
|
62
|
+
end
|
63
|
+
|
64
|
+
def reinitialize!
|
65
|
+
@world = nil
|
66
|
+
self.initialize!
|
67
|
+
end
|
68
|
+
|
69
|
+
def world
|
70
|
+
return @world if @world
|
71
|
+
|
72
|
+
initialize! if config.lazy_initialization
|
73
|
+
unless @world
|
74
|
+
raise 'The Dynflow world was not initialized yet. '\
|
75
|
+
'If your plugin uses it, make sure to call ForemanTasks.dynflow.require! '\
|
76
|
+
'in some initializer'
|
77
|
+
end
|
78
|
+
|
79
|
+
return @world
|
80
|
+
end
|
81
|
+
|
82
|
+
def web_console
|
83
|
+
::Dynflow::WebConsole.setup do
|
84
|
+
before do
|
85
|
+
# TODO: propper authentication
|
86
|
+
User.current = User.first
|
87
|
+
end
|
88
|
+
|
89
|
+
set(:world) { ForemanTasks.dynflow.world }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def eager_load_actions!
|
94
|
+
config.eager_load_paths.each do |load_path|
|
95
|
+
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
|
96
|
+
require_dependency file
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
class Dynflow::Configuration
|
3
|
+
|
4
|
+
# for logging action related info (such as exceptions raised in side
|
5
|
+
# the actions' methods
|
6
|
+
attr_accessor :action_logger
|
7
|
+
|
8
|
+
# for logging dynflow related info about the progress of the execution etc.
|
9
|
+
attr_accessor :dynflow_logger
|
10
|
+
|
11
|
+
# the number of threads in the pool handling the execution
|
12
|
+
attr_accessor :pool_size
|
13
|
+
|
14
|
+
# set true if the executor runs externally (by default true in procution, othewise false)
|
15
|
+
attr_accessor :remote
|
16
|
+
alias_method :remote?, :remote
|
17
|
+
|
18
|
+
# if remote set to true, use this path for socket communication
|
19
|
+
# between this process and the external executor
|
20
|
+
attr_accessor :remote_socket_path
|
21
|
+
|
22
|
+
# what persistence adapater should be used, by default, it uses Sequlel
|
23
|
+
# adapter based on Rails app database.yml configuration
|
24
|
+
attr_accessor :persistence_adapter
|
25
|
+
|
26
|
+
# what transaction adapater should be used, by default, it uses the ActiveRecord
|
27
|
+
# based adapter, expecting ActiveRecord is used as ORM in the application
|
28
|
+
attr_accessor :transaction_adapter
|
29
|
+
|
30
|
+
attr_accessor :eager_load_paths
|
31
|
+
|
32
|
+
attr_accessor :lazy_initialization
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
self.action_logger = Rails.logger
|
36
|
+
self.dynflow_logger = Rails.logger
|
37
|
+
self.pool_size = 5
|
38
|
+
self.remote = Rails.env.production?
|
39
|
+
self.remote_socket_path = File.join(Rails.root, "tmp", "sockets", "dynflow_socket")
|
40
|
+
self.persistence_adapter = default_persistence_adapter
|
41
|
+
self.transaction_adapter = ::Dynflow::TransactionAdapters::ActiveRecord.new
|
42
|
+
self.eager_load_paths = []
|
43
|
+
self.lazy_initialization = !Rails.env.production?
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize_world(world_class = ::Dynflow::World)
|
47
|
+
world_class.new(world_options)
|
48
|
+
end
|
49
|
+
|
50
|
+
# No matter what config.remote says, when the process is marked as executor,
|
51
|
+
# it can't be remote
|
52
|
+
def remote?
|
53
|
+
!ForemanTasks.dynflow.executor? && @remote
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
# generates the options hash consumable by the Dynflow's world
|
59
|
+
def world_options
|
60
|
+
{ logger_adapter: ::Dynflow::LoggerAdapters::Delegator.new(action_logger, dynflow_logger),
|
61
|
+
pool_size: 5,
|
62
|
+
persistence_adapter: persistence_adapter,
|
63
|
+
transaction_adapter: transaction_adapter,
|
64
|
+
executor: -> world { initialize_executor world } }
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_persistence_adapter
|
68
|
+
ForemanTasks::Dynflow::Persistence.new(default_sequel_adapter_options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_sequel_adapter_options
|
72
|
+
db_config = ActiveRecord::Base.configurations[Rails.env].dup
|
73
|
+
db_config['adapter'] = 'postgres' if db_config['adapter'] == 'postgresql'
|
74
|
+
db_config['adapter'] = 'sqlite' if db_config['adapter'] == 'sqlite3'
|
75
|
+
return db_config
|
76
|
+
end
|
77
|
+
|
78
|
+
def initialize_executor(world)
|
79
|
+
if self.remote?
|
80
|
+
::Dynflow::Executors::RemoteViaSocket.new(world, self.remote_socket_path)
|
81
|
+
else
|
82
|
+
::Dynflow::Executors::Parallel.new(world, self.pool_size)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
class Dynflow::Daemon
|
3
|
+
|
4
|
+
# load the Rails environment and initialize the executor and listener
|
5
|
+
# in this thread.
|
6
|
+
def run(foreman_root = Dir.pwd)
|
7
|
+
STDERR.puts("Starting Rails environment")
|
8
|
+
foreman_env_file = File.expand_path("./config/environment.rb", foreman_root)
|
9
|
+
unless File.exists?(foreman_env_file)
|
10
|
+
raise "#{foreman_root} doesn't seem to be a foreman root directory"
|
11
|
+
end
|
12
|
+
ForemanTasks.dynflow.executor!
|
13
|
+
require foreman_env_file
|
14
|
+
STDERR.puts("Starting listener")
|
15
|
+
daemon = ::Dynflow::Daemon.new(listener, world, lock_file)
|
16
|
+
STDERR.puts("Everything ready")
|
17
|
+
daemon.run
|
18
|
+
end
|
19
|
+
|
20
|
+
# run the executor as a daemon
|
21
|
+
def run_background(command = "start", options = {})
|
22
|
+
default_options = { foreman_root: Dir.pwd,
|
23
|
+
process_name: 'dynflow_executor',
|
24
|
+
pid_dir: "#{Rails.root}/tmp/pids",
|
25
|
+
wait_attempts: 300,
|
26
|
+
wait_sleep: 1 }
|
27
|
+
options = default_options.merge(options)
|
28
|
+
begin
|
29
|
+
require 'daemons'
|
30
|
+
rescue LoadError
|
31
|
+
raise "You need to add gem 'daemons' to your Gemfile if you wish to use it."
|
32
|
+
end
|
33
|
+
|
34
|
+
unless %w[start stop restart run].include?(command)
|
35
|
+
raise "Command exptected to be 'start', 'stop', 'restart', 'run', was #{command.inspect}"
|
36
|
+
end
|
37
|
+
|
38
|
+
STDERR.puts("Dynflow Executor: #{command} in progress")
|
39
|
+
|
40
|
+
Daemons.run_proc(options[:process_name],
|
41
|
+
:dir => options[:pid_dir],
|
42
|
+
:dir_mode => :normal,
|
43
|
+
:monitor => true,
|
44
|
+
:log_output => true,
|
45
|
+
:ARGV => [command]) do |*args|
|
46
|
+
begin
|
47
|
+
run(options[:foreman_root])
|
48
|
+
rescue => e
|
49
|
+
STDERR.puts e.message
|
50
|
+
Rails.logger.fatal e
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
if command == "start" || command == "restart"
|
55
|
+
STDERR.puts('Waiting for the executor to be ready...')
|
56
|
+
options[:wait_attempts].times do |i|
|
57
|
+
STDERR.print('.')
|
58
|
+
if File.exists?(lock_file)
|
59
|
+
STDERR.puts('executor started successfully')
|
60
|
+
break
|
61
|
+
else
|
62
|
+
sleep options[:wait_sleep]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def listener
|
71
|
+
::Dynflow::Listeners::Socket.new(world, socket_path)
|
72
|
+
end
|
73
|
+
|
74
|
+
def socket_path
|
75
|
+
ForemanTasks.dynflow.config.remote_socket_path
|
76
|
+
end
|
77
|
+
|
78
|
+
def lock_file
|
79
|
+
File.join(Rails.root, 'tmp', 'dynflow_executor.lock')
|
80
|
+
end
|
81
|
+
|
82
|
+
def world
|
83
|
+
ForemanTasks.dynflow.world
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|