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,20 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
module Concerns
|
3
|
+
module ArchitectureActionSubject
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ForemanTasks::Concerns::ActionSubject
|
6
|
+
|
7
|
+
def create_action
|
8
|
+
::Actions::Foreman::Architecture::Create
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_action
|
12
|
+
::Actions::Foreman::Architecture::Update
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy_action
|
16
|
+
::Actions::Foreman::Architecture::Destroy
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
module Concerns
|
3
|
+
module HostActionSubject
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ForemanTasks::Concerns::ActionSubject
|
6
|
+
|
7
|
+
def action_input_key
|
8
|
+
"host"
|
9
|
+
end
|
10
|
+
|
11
|
+
def available_locks
|
12
|
+
[:read, :write, :import_facts]
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# TODO: This should get into the Foreman core, extracting the
|
17
|
+
# +importHostAndFacts+ method into two
|
18
|
+
def importHost(hostname, certname, proxy_id = nil)
|
19
|
+
raise(::Foreman::Exception.new("Invalid Hostname, must be a String")) unless hostname.is_a?(String)
|
20
|
+
|
21
|
+
# downcase everything
|
22
|
+
hostname.try(:downcase!)
|
23
|
+
certname.try(:downcase!)
|
24
|
+
|
25
|
+
host = certname.present? ? Host.find_by_certname(certname) : nil
|
26
|
+
host ||= Host.find_by_name hostname
|
27
|
+
host ||= Host.new(:name => hostname, :certname => certname) if Setting[:create_new_host_when_facts_are_uploaded]
|
28
|
+
if host
|
29
|
+
# if we were given a certname but found the Host by hostname we should update the certname
|
30
|
+
host.certname = certname if certname.present?
|
31
|
+
# if proxy authentication is enabled and we have no puppet proxy set, use it.
|
32
|
+
host.puppet_proxy_id ||= proxy_id
|
33
|
+
host.save(:validate => false)
|
34
|
+
return host
|
35
|
+
else
|
36
|
+
return
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
class Lock < ActiveRecord::Base
|
3
|
+
|
4
|
+
LINK_LOCK_NAME = :link_resource
|
5
|
+
OWNER_LOCK_NAME = :task_owner
|
6
|
+
|
7
|
+
# not really intedet to be created in database, but it's used for
|
8
|
+
# explicitly stating that the all the locks for resource should be used
|
9
|
+
ALL_LOCK_NAME = :all
|
10
|
+
|
11
|
+
RESERVED_LOCK_NAMES = [LINK_LOCK_NAME, OWNER_LOCK_NAME, ALL_LOCK_NAME]
|
12
|
+
|
13
|
+
class LockConflict < StandardError
|
14
|
+
attr_reader :required_lock, :conflicting_locks
|
15
|
+
def initialize(required_lock, conflicting_locks)
|
16
|
+
super()
|
17
|
+
@required_lock = required_lock
|
18
|
+
@conflicting_locks = conflicting_locks
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
belongs_to :task
|
23
|
+
|
24
|
+
belongs_to :resource, polymorphic: true
|
25
|
+
|
26
|
+
scope :active, -> do
|
27
|
+
joins(:task).where('foreman_tasks_tasks.state != ?', :stopped)
|
28
|
+
end
|
29
|
+
|
30
|
+
validates :task_id, :name, :resource_id, :resource_type, presence: true
|
31
|
+
|
32
|
+
validate do
|
33
|
+
unless available?
|
34
|
+
raise LockConflict.new(self, coliding_locks)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# returns true if it's possible to aquire this kind of lock
|
39
|
+
def available?
|
40
|
+
return true unless coliding_locks.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
# returns a scope of the locks coliding with this one
|
44
|
+
def coliding_locks
|
45
|
+
coliding_locks_scope = Lock.active.where('foreman_tasks_locks.task_id != ?', task_id)
|
46
|
+
coliding_locks_scope = coliding_locks_scope.where(name: name,
|
47
|
+
resource_id: resource_id,
|
48
|
+
resource_type: resource_type)
|
49
|
+
unless self.exclusive?
|
50
|
+
coliding_locks_scope = coliding_locks_scope.where(:exclusive => true)
|
51
|
+
end
|
52
|
+
return coliding_locks_scope
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
|
57
|
+
# Locks the resource so that no other task can lock it while running.
|
58
|
+
# No other task related to the resource is not allowed (even not-locking ones)
|
59
|
+
# A typical usecase is resource deletion, where it's good idea to make sure
|
60
|
+
# nothing else related to the resource is really running.
|
61
|
+
def exclusive!(resource, uuid)
|
62
|
+
build_exclusive_locks(resource, uuid).each(&:save!)
|
63
|
+
end
|
64
|
+
|
65
|
+
def exclusive?(resource)
|
66
|
+
build_exclusive_locks(resource).all?(:available?)
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Locks the resource so that no other task can lock it while running.
|
71
|
+
# Other not-locking tasks are tolerated.
|
72
|
+
#
|
73
|
+
# The lock names allow to specify what locks should be activated. It has to
|
74
|
+
# be a subset of names defined in model's class available_locks method
|
75
|
+
#
|
76
|
+
# When no lock name is specified, the resource is locked against all the available
|
77
|
+
# locks.
|
78
|
+
#
|
79
|
+
# It also looks at +related_resources+ method of the resource to calcuate all
|
80
|
+
# the related resources (recursively) and links the task to them as well.
|
81
|
+
def lock!(resource, uuid, *lock_names)
|
82
|
+
build_locks(resource, lock_names, uuid).each(&:save!)
|
83
|
+
end
|
84
|
+
|
85
|
+
def lock?(resource, uuid, *lock_names)
|
86
|
+
build_locks(resource, lock_names, uuid).all?(&:available?)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Assigns the resource to the task to easily track the task in context of
|
90
|
+
# the resource. This doesn't prevent other actions to lock the resource
|
91
|
+
# and should be used only for actions that tolerate other actions to be
|
92
|
+
# performed on the resource. Usually, this shouldn't needed to be done
|
93
|
+
# through the action directly, because the lock should assign it's parrent
|
94
|
+
# objects to the action srecursively (using +related_resources+ method in model
|
95
|
+
# objects)
|
96
|
+
def link!(resource, uuid)
|
97
|
+
build_link(resource, uuid).save!
|
98
|
+
end
|
99
|
+
|
100
|
+
def link?(resource, uuid)
|
101
|
+
build_link(resource, uuid).available?
|
102
|
+
end
|
103
|
+
|
104
|
+
# Records the information about the user that triggered the task
|
105
|
+
def owner!(user, uuid)
|
106
|
+
build_owner(user, uuid).save!
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def all_lock_names(resource, include_links = false)
|
112
|
+
lock_names = []
|
113
|
+
if resource.class.respond_to?(:available_locks) &&
|
114
|
+
resource.class.available_locks.any?
|
115
|
+
lock_names.concat(resource.class.available_locks)
|
116
|
+
else
|
117
|
+
raise "The resource #{resource.class.name} doesn't define any available lock"
|
118
|
+
end
|
119
|
+
if lock_names.any? { |lock_name| RESERVED_LOCK_NAMES.include?(lock_name) }
|
120
|
+
raise "Lock name #{lock_name} is reserved"
|
121
|
+
end
|
122
|
+
lock_names.concat([LINK_LOCK_NAME, OWNER_LOCK_NAME]) if include_links
|
123
|
+
return lock_names
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_exclusive_locks(resource, uuid = nil)
|
127
|
+
build_locks(resource, all_lock_names(resource, true), uuid)
|
128
|
+
end
|
129
|
+
|
130
|
+
def build_locks(resource, lock_names, uuid = nil)
|
131
|
+
locks = []
|
132
|
+
if lock_names.empty? || lock_names == [:all]
|
133
|
+
lock_names = all_lock_names(resource)
|
134
|
+
end
|
135
|
+
lock_names.map do |lock_name|
|
136
|
+
locks << build(uuid, resource, lock_name, true)
|
137
|
+
end
|
138
|
+
locks.concat(build_links(resource, uuid))
|
139
|
+
return locks
|
140
|
+
end
|
141
|
+
|
142
|
+
def build_links(resource, uuid = nil)
|
143
|
+
related_resources(resource).map do |related_resource|
|
144
|
+
build_link(related_resource, uuid)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_link(resource, uuid = nil)
|
149
|
+
build(uuid, resource, LINK_LOCK_NAME, false)
|
150
|
+
end
|
151
|
+
|
152
|
+
def build_owner(user, uuid = nil)
|
153
|
+
build(uuid, user, OWNER_LOCK_NAME, false)
|
154
|
+
end
|
155
|
+
|
156
|
+
def build(uuid, resource, lock_name, exclusive)
|
157
|
+
self.new(task_id: uuid,
|
158
|
+
name: lock_name,
|
159
|
+
resource_type: resource.class.name,
|
160
|
+
resource_id: resource.id,
|
161
|
+
exclusive: !!exclusive)
|
162
|
+
end
|
163
|
+
|
164
|
+
# recursively search for related resources of the resource (using
|
165
|
+
# the +related_resources+ method, avoiding the cycles
|
166
|
+
def related_resources(resource)
|
167
|
+
if resource.respond_to?(:related_resources_recursive)
|
168
|
+
return resource.related_resources_recursive
|
169
|
+
else
|
170
|
+
return []
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'uuidtools'
|
2
|
+
|
3
|
+
module ForemanTasks
|
4
|
+
class Task < ActiveRecord::Base
|
5
|
+
|
6
|
+
self.primary_key = :id
|
7
|
+
before_create :generate_id
|
8
|
+
|
9
|
+
has_many :locks
|
10
|
+
|
11
|
+
# in fact, the task has only one owner but Rails don't let you to
|
12
|
+
# specify has_one relation though has_many relation
|
13
|
+
has_many :owners, :through => :locks, :source => :resource, :source_type => 'User'
|
14
|
+
|
15
|
+
scoped_search :on => :id, :complete_value => false
|
16
|
+
scoped_search :on => :label, :complete_value => true
|
17
|
+
scoped_search :on => :state, :complete_value => true
|
18
|
+
scoped_search :on => :result, :complete_value => true
|
19
|
+
scoped_search :on => :started_at, :complete_value => false
|
20
|
+
scoped_search :in => :locks, :on => :resource_type, :complete_value => true, :rename => "resource_type", :ext_method => :search_by_generic_resource
|
21
|
+
scoped_search :in => :locks, :on => :resource_id, :complete_value => false, :rename => "resource_id", :ext_method => :search_by_generic_resource
|
22
|
+
scoped_search :in => :owners, :on => :login, :complete_value => true, :rename => "owner.login", :ext_method => :search_by_owner
|
23
|
+
scoped_search :in => :owners, :on => :firstname, :complete_value => true, :rename => "owner.firstname", :ext_method => :search_by_owner
|
24
|
+
|
25
|
+
|
26
|
+
scope :active, -> { where('state != ?', :stopped) }
|
27
|
+
|
28
|
+
def input
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
|
32
|
+
def output
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
|
36
|
+
def owner
|
37
|
+
self.owners.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def username
|
41
|
+
self.owner.try(:login)
|
42
|
+
end
|
43
|
+
|
44
|
+
def humanized
|
45
|
+
{ action: label,
|
46
|
+
input: "",
|
47
|
+
output: "" }
|
48
|
+
end
|
49
|
+
|
50
|
+
def cli_example
|
51
|
+
""
|
52
|
+
end
|
53
|
+
|
54
|
+
# returns true if the task is running or waiting to be run
|
55
|
+
def pending
|
56
|
+
self.state != 'stopped'
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.search_by_generic_resource(key, operator, value)
|
60
|
+
key_name = self.connection.quote_column_name(key.sub(/^.*\./,''))
|
61
|
+
condition = sanitize_sql_for_conditions(["foreman_tasks_locks.#{key_name} #{operator} ?", value])
|
62
|
+
|
63
|
+
return {:conditions => condition, :joins => :locks }
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.search_by_owner(key, operator, value)
|
67
|
+
key_name = self.connection.quote_column_name(key.sub(/^.*\./,''))
|
68
|
+
joins = <<-JOINS
|
69
|
+
INNER JOIN foreman_tasks_locks AS foreman_tasks_locks_owner
|
70
|
+
ON (foreman_tasks_locks_owner.task_id = foreman_tasks_tasks.id AND
|
71
|
+
foreman_tasks_locks_owner.resource_type = 'User' AND
|
72
|
+
foreman_tasks_locks_owner.name = '#{Lock::OWNER_LOCK_NAME}')
|
73
|
+
INNER JOIN users
|
74
|
+
ON (users.id = foreman_tasks_locks_owner.resource_id)
|
75
|
+
JOINS
|
76
|
+
condition = sanitize_sql_for_conditions(["users.#{key_name} #{operator} ?", value])
|
77
|
+
return {:conditions => condition, :joins => joins }
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def generate_id
|
83
|
+
self.id ||= UUIDTools::UUID.random_create.to_s
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
class Task::DynflowTask < ForemanTasks::Task
|
3
|
+
|
4
|
+
def update_from_dynflow(data, planned)
|
5
|
+
self.external_id = data[:id]
|
6
|
+
self.started_at = data[:started_at]
|
7
|
+
self.ended_at = data[:ended_at]
|
8
|
+
self.state = data[:state].to_s
|
9
|
+
self.result = data[:result].to_s
|
10
|
+
|
11
|
+
if planned
|
12
|
+
# for now, this part needs to laod the execution_plan to
|
13
|
+
# load extra data, there is place for optimization on Dynflow side
|
14
|
+
# if needed (getting more keys into the data value)
|
15
|
+
unless self.label
|
16
|
+
self.label = main_action.action_class.name
|
17
|
+
end
|
18
|
+
update_progress
|
19
|
+
end
|
20
|
+
self.save!
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_progress
|
24
|
+
self.progress = execution_plan.progress
|
25
|
+
end
|
26
|
+
|
27
|
+
def execution_plan
|
28
|
+
@execution_plan ||= ForemanTasks.dynflow.world.persistence.load_execution_plan(external_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def main_action
|
32
|
+
return @main_action if @main_action
|
33
|
+
main_action_id = execution_plan.root_plan_step.action_id
|
34
|
+
@main_action = execution_plan.actions.find { |action| action.id == main_action_id }
|
35
|
+
end
|
36
|
+
|
37
|
+
def input
|
38
|
+
main_action.respond_to?(:task_input) && main_action.task_input
|
39
|
+
end
|
40
|
+
|
41
|
+
def output
|
42
|
+
main_action.respond_to?(:task_output) && main_action.task_output
|
43
|
+
end
|
44
|
+
|
45
|
+
def humanized
|
46
|
+
{ action: main_action.respond_to?(:humanized_name) && main_action.humanized_name,
|
47
|
+
input: main_action.respond_to?(:humanized_input) && main_action.humanized_input,
|
48
|
+
output: main_action.respond_to?(:humanized_output) && main_action.humanized_output }
|
49
|
+
end
|
50
|
+
|
51
|
+
def cli_example
|
52
|
+
if main_action.respond_to?(:cli_example)
|
53
|
+
main_action.cli_example
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def main_action
|
60
|
+
return @main_action if @main_action
|
61
|
+
main_action_id = execution_plan.root_plan_step.action_id
|
62
|
+
@main_action = execution_plan.actions.find { |action| action.id == main_action_id }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<% title _("Tasks") %>
|
2
|
+
<% title_actions help_path%>
|
3
|
+
|
4
|
+
<style>
|
5
|
+
.task-id {
|
6
|
+
white-space: nowrap;
|
7
|
+
}
|
8
|
+
.param-name {
|
9
|
+
display: inline-block;
|
10
|
+
width: 10em;
|
11
|
+
}
|
12
|
+
|
13
|
+
.task-details pre {
|
14
|
+
white-space: pre;
|
15
|
+
word-break: normal;
|
16
|
+
}
|
17
|
+
</style>
|
18
|
+
|
19
|
+
<script>
|
20
|
+
$(document).on('click', ".table-two-pane td.two-pane-link", function(e) {
|
21
|
+
var item = $(this).find("a");
|
22
|
+
if(item.length){
|
23
|
+
e.preventDefault();
|
24
|
+
two_pane_open(item);
|
25
|
+
}
|
26
|
+
});
|
27
|
+
|
28
|
+
</script>
|
29
|
+
<table class="table table-bordered table-striped table-two-pane">
|
30
|
+
<tr>
|
31
|
+
<th><%= _("Action") %></th>
|
32
|
+
<th><%= _("State") %></th>
|
33
|
+
<th><%= _("Result") %></th>
|
34
|
+
<th><%= sort :started_at, :as => _("Started at") %></th>
|
35
|
+
<th><%= _("User") %></th>
|
36
|
+
</tr>
|
37
|
+
<% for task in @tasks %>
|
38
|
+
<tr>
|
39
|
+
<td class="task-id two-pane-link">
|
40
|
+
<%= link_to_if_authorized(format_task_input(task, true),
|
41
|
+
hash_for_foreman_tasks_task_path(:id => task)) %>
|
42
|
+
</td>
|
43
|
+
<td><%= task.state %></td>
|
44
|
+
<td><%= task.result %></td>
|
45
|
+
<td><%= task.started_at %></td>
|
46
|
+
<td><%= task.username %></td>
|
47
|
+
</tr>
|
48
|
+
<% end %>
|
49
|
+
</table>
|
50
|
+
<%= page_entries_info @tasks %>
|
51
|
+
<%= will_paginate @tasks %>
|