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,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,5 @@
1
+ object @task if @task
2
+
3
+ attributes :id, :label, :pending
4
+ attributes :username, :started_at, :ended_at, :state, :result, :progress
5
+ attributes :input, :output, :humanized, :cli_example
@@ -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 %>