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,29 @@
|
|
1
|
+
module Actions
|
2
|
+
module Foreman
|
3
|
+
module Architecture
|
4
|
+
class Create < Actions::EntryAction
|
5
|
+
|
6
|
+
def plan(architecture)
|
7
|
+
action_subject(architecture, :changes => architecture.changes)
|
8
|
+
end
|
9
|
+
|
10
|
+
def humanized_name
|
11
|
+
_("Create architecture")
|
12
|
+
end
|
13
|
+
|
14
|
+
def humanized_input
|
15
|
+
input[:architecture] && input[:architecture][:name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def cli_example
|
19
|
+
return unless input[:architecture]
|
20
|
+
<<-EXAMPLE
|
21
|
+
hammer architecture create --id '#{task_input[:architecture][:id]}' \
|
22
|
+
--name '#{task_input[:architecture][:name]}'
|
23
|
+
EXAMPLE
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Actions
|
2
|
+
module Foreman
|
3
|
+
module Architecture
|
4
|
+
class Destroy < Actions::EntryAction
|
5
|
+
|
6
|
+
def plan(architecture)
|
7
|
+
action_subject(architecture)
|
8
|
+
end
|
9
|
+
|
10
|
+
def humanized_name
|
11
|
+
_("Delete architecture")
|
12
|
+
end
|
13
|
+
|
14
|
+
def humanized_input
|
15
|
+
input[:architecture] && input[:architecture][:name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def cli_example
|
19
|
+
return unless input[:architecture]
|
20
|
+
<<-EXAMPLE
|
21
|
+
hammer architecture delete --id '#{task_input[:architecture][:id]}'
|
22
|
+
EXAMPLE
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Actions
|
2
|
+
module Foreman
|
3
|
+
module Architecture
|
4
|
+
class Update < Actions::EntryAction
|
5
|
+
|
6
|
+
def plan(architecture)
|
7
|
+
action_subject(architecture, :changes => architecture.changes)
|
8
|
+
end
|
9
|
+
|
10
|
+
def humanized_name
|
11
|
+
_("Update architecture")
|
12
|
+
end
|
13
|
+
|
14
|
+
def humanized_input
|
15
|
+
input[:architecture] && input[:architecture][:name]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Actions
|
2
|
+
module Foreman
|
3
|
+
module Host
|
4
|
+
|
5
|
+
class ImportFacts < Actions::EntryAction
|
6
|
+
|
7
|
+
def resource_locks
|
8
|
+
:import_facts
|
9
|
+
end
|
10
|
+
|
11
|
+
def plan(host_type, host_name, facts, certname, proxy_id)
|
12
|
+
host = ::Host::Base.importHost(host_name, certname, proxy_id)
|
13
|
+
action_subject(host, :facts => facts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
::User.as :admin do
|
18
|
+
host = ::Host.find(input[:host][:id])
|
19
|
+
state = host.importFacts(input[:facts])
|
20
|
+
output[:state] = state
|
21
|
+
end
|
22
|
+
rescue ::Foreman::Exception => e
|
23
|
+
# This error is what is thrown by Host#ImportHostAndFacts when
|
24
|
+
# the Host is in the build state. This can be refactored once
|
25
|
+
# issue #3959 is fixed.
|
26
|
+
raise e unless e.code == 'ERF51-9911'
|
27
|
+
end
|
28
|
+
|
29
|
+
def humanized_name
|
30
|
+
_("Import facts")
|
31
|
+
end
|
32
|
+
|
33
|
+
def humanized_input
|
34
|
+
input[:host] && input[:host][:name]
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Actions
|
2
|
+
module Helpers
|
3
|
+
module ArgsSerialization
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
class Builder
|
9
|
+
attr_reader :hash
|
10
|
+
def initialize(*objects)
|
11
|
+
@hash = {}.with_indifferent_access
|
12
|
+
objects.each do |object|
|
13
|
+
add_object(object)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def add_object(object)
|
20
|
+
case object
|
21
|
+
when ActiveRecord::Base
|
22
|
+
unless object.respond_to?(:action_input_key)
|
23
|
+
raise "Serialized model has to repond to :action_input_key method"
|
24
|
+
end
|
25
|
+
key = object.action_input_key
|
26
|
+
value = object_to_value(object)
|
27
|
+
add(key, value)
|
28
|
+
when Hash
|
29
|
+
add_hash(object_to_value(object))
|
30
|
+
else
|
31
|
+
raise "don't know how to serialize #{object.inspect}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def object_to_value(object)
|
36
|
+
case object
|
37
|
+
when Array
|
38
|
+
object.map { |item| object_to_value(item) }
|
39
|
+
when Hash
|
40
|
+
object.reduce({}) do |new_hash, (key, value)|
|
41
|
+
new_hash.update(key => object_to_value(value))
|
42
|
+
end
|
43
|
+
when ActiveRecord::Base
|
44
|
+
unless object.respond_to?(:to_action_input)
|
45
|
+
raise "Serialized model has to repond to :to_action_input method"
|
46
|
+
end
|
47
|
+
object.to_action_input
|
48
|
+
when String, Numeric, true, false, nil
|
49
|
+
object
|
50
|
+
else
|
51
|
+
object.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_hash(hash)
|
56
|
+
hash.each do |key, value|
|
57
|
+
add(key, value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add(key, value)
|
62
|
+
if hash.has_key?(key)
|
63
|
+
raise "Conflict while serializing action args in key #{key}"
|
64
|
+
end
|
65
|
+
hash.update(key => value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module ClassMethods
|
70
|
+
|
71
|
+
def generate_phase(phase_module)
|
72
|
+
super.tap do |phase_class|
|
73
|
+
if phase_module == Dynflow::Action::PlanPhase
|
74
|
+
phase_class.send(:include, PlanMethods)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
module PlanMethods
|
82
|
+
|
83
|
+
def serialize_args(*objects)
|
84
|
+
Builder.new(*objects).hash
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Actions
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
class Humanizer
|
5
|
+
|
6
|
+
PARTS_ORDER = [:repository,
|
7
|
+
:product,
|
8
|
+
:system,
|
9
|
+
:organization]
|
10
|
+
# Just to get the trings into pot file
|
11
|
+
PARTS_TRANSLATIONS = [N_('repository'),
|
12
|
+
N_('product'),
|
13
|
+
N_('system'),
|
14
|
+
N_('organization')]
|
15
|
+
|
16
|
+
def initialize(action)
|
17
|
+
@action = action
|
18
|
+
@input = action.respond_to?(:task_input) ? action.task_input : action.input
|
19
|
+
@input ||= {}
|
20
|
+
@output = action.respond_to?(:task_output) ? action.task_output : action.output
|
21
|
+
@output ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def input(*parts)
|
25
|
+
if parts.empty?
|
26
|
+
parts = PARTS_ORDER
|
27
|
+
end
|
28
|
+
included_parts(parts, @input).map do |part|
|
29
|
+
[part, humanize_resource(part, @input[part], @input)]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def included_parts(parts, data)
|
34
|
+
parts.select { |part| data.has_key?(part) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def humanize_resource(type, data, other_data)
|
38
|
+
humanized_type = _(type)
|
39
|
+
humanized_value = data[:name] || data[:label] || data[:id]
|
40
|
+
{ text: "#{humanized_type} '#{humanized_value}'",
|
41
|
+
link: link_to_resource(type, data, other_data) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def link_to_resource(type, data, other_data)
|
45
|
+
case type
|
46
|
+
when :product
|
47
|
+
"#/products/#{data[:cp_id]}/info" if data[:cp_id]
|
48
|
+
when :repository
|
49
|
+
if other_data[:product] && other_data[:product][:cp_id] && data[:id]
|
50
|
+
"#/products/#{other_data[:product][:cp_id]}/repositories/#{data[:id]}"
|
51
|
+
end
|
52
|
+
when :system
|
53
|
+
if data[:uuid]
|
54
|
+
"#/systems/#{data[:uuid]}/info"
|
55
|
+
end
|
56
|
+
when :organization
|
57
|
+
if data[:label]
|
58
|
+
"/katello/organizations#/!=&panel=organization_#{data[:label]}&!="
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Actions
|
2
|
+
module Helpers
|
3
|
+
module Lock
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def generate_phase(phase_module)
|
11
|
+
super.tap do |phase_class|
|
12
|
+
if phase_module == Dynflow::Action::PlanPhase
|
13
|
+
phase_class.send(:include, PlanMethods)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
module PlanMethods
|
21
|
+
|
22
|
+
def task
|
23
|
+
::ForemanTasks::Task::DynflowTask.find_by_external_id!(execution_plan_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @see Lock.exclusive!
|
27
|
+
def exclusive_lock!(resource)
|
28
|
+
::ForemanTasks::Lock.exclusive!(resource, task.id)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see Lock.lock!
|
32
|
+
def lock!(resource, *lock_names)
|
33
|
+
::ForemanTasks::Lock.lock!(resource, task.id, *lock_names.flatten)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @see Lock.link!
|
37
|
+
def link!(resource)
|
38
|
+
::ForemanTasks::Lock.link!(resource, task.id)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Actions
|
2
|
+
class TestAction < Base
|
3
|
+
|
4
|
+
def run(event = nil)
|
5
|
+
case event
|
6
|
+
when nil
|
7
|
+
suspend do |suspended_action|
|
8
|
+
puts "Execution plan id is #{suspended_action.execution_plan_id}"
|
9
|
+
puts "Step id is #{suspended_action.step_id}"
|
10
|
+
end
|
11
|
+
else
|
12
|
+
puts event.inspect
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
module Concerns
|
3
|
+
module ActionSubject
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
after_create :plan_create_action
|
9
|
+
after_update :plan_update_action
|
10
|
+
after_destroy :plan_destroy_action
|
11
|
+
after_commit :execute_planned_action
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def available_locks
|
16
|
+
[:read, :write]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def action_input_key
|
21
|
+
self.class.name.underscore[/\w*\Z/]
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_action_input
|
25
|
+
if self.new_record?
|
26
|
+
raise "The resource needs to be saved first"
|
27
|
+
end
|
28
|
+
{ id: id, name: name }.tap do |hash|
|
29
|
+
hash.update(label: label) if self.respond_to? :label
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api override to return the objects that relate to this one, usually parent
|
34
|
+
# objects, e.g. repository would return product it belongs to, product would return
|
35
|
+
# provider etc.
|
36
|
+
#
|
37
|
+
# It's used to link a task running on top of this resource to it's related objects,
|
38
|
+
# so that is't possible to see all the sync tasks for a product etc.
|
39
|
+
def related_resources
|
40
|
+
end
|
41
|
+
|
42
|
+
# Recursively searches for related resources of this one, avoiding cycles
|
43
|
+
def related_resources_recursive(result = [])
|
44
|
+
Array(related_resources).each do |related_resource|
|
45
|
+
unless result.include?(related_resource)
|
46
|
+
result << related_resource
|
47
|
+
if related_resource.respond_to?(:related_resources_recursive)
|
48
|
+
related_resource.related_resources_recursive(result)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
return result
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_action
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_action
|
59
|
+
end
|
60
|
+
|
61
|
+
def destroy_action
|
62
|
+
end
|
63
|
+
|
64
|
+
def plan_create_action
|
65
|
+
plan_action(create_action, self) if create_action
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
|
69
|
+
def plan_update_action
|
70
|
+
plan_action(update_action, self) if update_action
|
71
|
+
return true
|
72
|
+
end
|
73
|
+
|
74
|
+
def plan_destroy_action
|
75
|
+
plan_action(destroy_action, self) if destroy_action
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Perform planning phase of the action tied with the model event.
|
80
|
+
# We do it separately from the execution phase, because the transaction
|
81
|
+
# of planning phase is expected to be commited when execution occurs. Also
|
82
|
+
# we want to be able to rollback the whole db operation when planning fails.
|
83
|
+
def plan_action(action_class, *args)
|
84
|
+
@execution_plan = ::ForemanTasks.dynflow.world.plan(action_class, *args)
|
85
|
+
planned = @execution_plan.state == :planned
|
86
|
+
unless planned
|
87
|
+
errors = @execution_plan.steps.values.map(&:error).compact
|
88
|
+
# we raise error so that the whole transaction is rollbacked
|
89
|
+
raise errors.map(&:message).join('; ')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Execute the prepared execution plan after the db transaction was commited
|
94
|
+
def execute_planned_action
|
95
|
+
if @execution_plan
|
96
|
+
::ForemanTasks.dynflow.world.execute(@execution_plan.id)
|
97
|
+
end
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|