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,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