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.
Files changed (41) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +139 -0
  3. data/app/controllers/foreman_tasks/api/tasks_controller.rb +140 -0
  4. data/app/controllers/foreman_tasks/concerns/hosts_controller_extension.rb +26 -0
  5. data/app/controllers/foreman_tasks/tasks_controller.rb +19 -0
  6. data/app/helpers/foreman_tasks/tasks_helper.rb +16 -0
  7. data/app/lib/actions/base.rb +36 -0
  8. data/app/lib/actions/entry_action.rb +51 -0
  9. data/app/lib/actions/foreman/architecture/create.rb +29 -0
  10. data/app/lib/actions/foreman/architecture/destroy.rb +28 -0
  11. data/app/lib/actions/foreman/architecture/update.rb +21 -0
  12. data/app/lib/actions/foreman/host/import_facts.rb +40 -0
  13. data/app/lib/actions/helpers/args_serialization.rb +91 -0
  14. data/app/lib/actions/helpers/humanizer.rb +64 -0
  15. data/app/lib/actions/helpers/lock.rb +43 -0
  16. data/app/lib/actions/test_action.rb +17 -0
  17. data/app/models/foreman_tasks/concerns/action_subject.rb +102 -0
  18. data/app/models/foreman_tasks/concerns/architecture_action_subject.rb +20 -0
  19. data/app/models/foreman_tasks/concerns/host_action_subject.rb +42 -0
  20. data/app/models/foreman_tasks/lock.rb +176 -0
  21. data/app/models/foreman_tasks/task.rb +86 -0
  22. data/app/models/foreman_tasks/task/dynflow_task.rb +65 -0
  23. data/app/views/foreman_tasks/api/tasks/show.json.rabl +5 -0
  24. data/app/views/foreman_tasks/tasks/index.html.erb +51 -0
  25. data/app/views/foreman_tasks/tasks/show.html.erb +77 -0
  26. data/bin/dynflow-executor +43 -0
  27. data/config/routes.rb +20 -0
  28. data/db/migrate/20131205204140_create_foreman_tasks.rb +15 -0
  29. data/db/migrate/20131209122644_create_foreman_tasks_locks.rb +12 -0
  30. data/lib/foreman-tasks.rb +1 -0
  31. data/lib/foreman_tasks.rb +20 -0
  32. data/lib/foreman_tasks/dynflow.rb +101 -0
  33. data/lib/foreman_tasks/dynflow/configuration.rb +86 -0
  34. data/lib/foreman_tasks/dynflow/daemon.rb +88 -0
  35. data/lib/foreman_tasks/dynflow/persistence.rb +36 -0
  36. data/lib/foreman_tasks/engine.rb +58 -0
  37. data/lib/foreman_tasks/tasks/dynflow.rake +7 -0
  38. data/lib/foreman_tasks/version.rb +3 -0
  39. data/test/tasks_test.rb +7 -0
  40. data/test/test_helper.rb +15 -0
  41. 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