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
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ Foreman Tasks
2
+ =============
3
+
4
+ Tasks management engine for Foreman. Gives you and overview of what's
5
+ happening/happened in your Foreman instance.
6
+
7
+ Installation
8
+ ------------
9
+
10
+ Put the following to your Foreman's bundle.d/Gemfile.local.rb:
11
+
12
+ ```ruby
13
+ gem 'dynflow', :git => 'git@github.com:iNecas/dynflow.git'
14
+ gem 'foreman-tasks', :git => 'git@github.com:iNecas/foreman-tasks.git'
15
+ ```
16
+
17
+ Run:
18
+
19
+ ```bash
20
+ bundle install
21
+ rake db:migrate
22
+ ```
23
+
24
+ Usage
25
+ -----
26
+
27
+ In the UI, go to `/foreman_tasks/tasks`. This should give a list of
28
+ tasks that were run in the system. It's possible to filter that using
29
+ scoped search. Possible searches:
30
+
31
+ ```
32
+ # search all tasks by user
33
+ owner.login = admin
34
+ # search all tasks on architecture with id 9
35
+ resource_type = Architecture and resource_id = 9
36
+ ```
37
+
38
+ Clicking on the action, it should provide more details.
39
+
40
+ Via API:
41
+
42
+ ```bash
43
+ curl -k -u admin:changeme\
44
+ https://foreman.example.com/foreman_tasks/api/tasks/b346db45-76fd-4217-9247-aac51b5cde4e -H 'Accept: application/json'
45
+ ```
46
+
47
+ Features
48
+ --------
49
+
50
+ * Current tasks progress
51
+ * Audit: tasks history for resources and users
52
+ * Possibility to generate CLI examples
53
+ * Locking: connection between task and resource: allows listing tasks
54
+ for a resource but also allows preventing to run two
55
+ conflicting tasks on one resource.
56
+ * Dynflow integration allowing async processing, workflows definitions etc.
57
+
58
+
59
+ Dynflow Integration
60
+ -------------------
61
+
62
+ This engine is agnostic on background processing tool and can be used
63
+ with anything that allows supports some kind of execution hooks.
64
+
65
+ On the other side, since we started this as part of Katello
66
+ integration with Dynflow, the dynflow adapters are already there.
67
+
68
+ Also, since dynflow has no additional dependencies in terms of another
69
+ database (tested mainly on Postgres), this gem ships the Dynflow
70
+ setting so that Dynflow can be used directly.
71
+
72
+ It's turned off by default, but you can turn that on with putting this
73
+ code somewhere in Rails initialization process. In case of an engine,
74
+ it would be:
75
+
76
+ ```ruby
77
+ initializer "your_engine.dynflow_initialize" do |app|
78
+ ForemanTasks.dynflow.require!
79
+ end
80
+ ```
81
+
82
+ Additionally, there are also examples of using Dynflow for async tasks
83
+ and auditing included in this repository. To enable them you just need
84
+ to set `FOREMAN_TASKS_MONKEYS` env variable to `true`
85
+
86
+ ```bash
87
+ FOREMAN_TASKS_MONKEYS=true bundle exec rails s
88
+ ```
89
+
90
+ The example for async tasks handling is the puppet facts import. Next
91
+ time puppet imports the facts to Foreman, the task should appear in
92
+ the tasks list.
93
+
94
+ The example for auditing features is the architecture model. On every
95
+ modification, there is a corresponding Dynflow action triggered. This
96
+ leads to it appearing in the tasks list as well, even there was no
97
+ async processing involved, but still using the same interface to
98
+ show the task.
99
+
100
+ The Dynflow console is accessible on `/foreman_tasks/dynflow` path.
101
+
102
+ ## Production mode
103
+
104
+ In development mode, the Dynflow executor is part of the web server
105
+ process. However, in production, it's more than suitable to have the
106
+ web server process separated from the async executor. Therefore,
107
+ Dynflow is set to use external process in production mode by default
108
+ (can be changed with `ForemanTasks.dynflow.config.remote = false`).
109
+
110
+ The executor process needs to be executed before the web server. You
111
+ can run it by:
112
+
113
+ ```
114
+ RAILS_ENV=production bundle exec rake foreman_tasks:dynflow:executor
115
+ ```
116
+
117
+ Also, there is a possibility to run the executor in daemonized mode
118
+ using the `dynflow-executor`. It expects to be executed from Foreman
119
+ rails root directory. See `-h` for more details and options
120
+
121
+ Documentation
122
+ -------------
123
+
124
+ TBD - dig into the code for now (happy hacking:)
125
+
126
+ Tests
127
+ -----
128
+
129
+ TBD
130
+
131
+ License
132
+ -------
133
+
134
+ MIT
135
+
136
+ Author
137
+ ------
138
+
139
+ Ivan Nečas
@@ -0,0 +1,140 @@
1
+ #
2
+ # Copyright 2013 Red Hat, Inc.
3
+ #
4
+ # This software is licensed to you under the GNU General Public
5
+ # License as published by the Free Software Foundation; either version
6
+ # 2 of the License (GPLv2) or (at your option) any later version.
7
+ # There is NO WARRANTY for this software, express or implied,
8
+ # including the implied warranties of MERCHANTABILITY,
9
+ # NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
10
+ # have received a copy of GPLv2 along with this software; if not, see
11
+ # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
12
+
13
+ module ForemanTasks
14
+ module Api
15
+ class TasksController < ::Api::V2::BaseController
16
+
17
+ # Foreman right now doesn't have mechanism to
18
+ # cause general BadRequest handling, resuing the Apipie::ParamError
19
+ # for now http://projects.theforeman.org/issues/3957
20
+ class BadRequest < Apipie::ParamError
21
+ end
22
+
23
+ before_filter :find_task, :only => [:show]
24
+
25
+ def show
26
+ end
27
+
28
+ api :POST, "/tasks/bulk_search", "List dynflow tasks for uuids"
29
+ param :searches, Array, :desc => 'List of uuids to fetch info about' do
30
+ param :search_id, String, :desc => <<-DESC
31
+ Arbitraty value for client to identify the the request parts with results.
32
+ It's passed in the results to be able to pair the requests and responses properly.
33
+ DESC
34
+ param :type, %w[user resource task]
35
+ param :task_id, String, :desc => <<-DESC
36
+ In case :type = 'task', find the task by the uuid
37
+ DESC
38
+ param :user_id, String, :desc => <<-DESC
39
+ In case :type = 'user', find tasks for the user
40
+ DESC
41
+ param :resource_type, String, :desc => <<-DESC
42
+ In case :type = 'resource', what resource type we're searching the tasks for
43
+ DESC
44
+ param :resource_type, String, :desc => <<-DESC
45
+ In case :type = 'resource', what resource id we're searching the tasks for
46
+ DESC
47
+ param :active_only, :bool
48
+ param :page, String
49
+ param :per_page, String
50
+ end
51
+ desc <<-DESC
52
+ For every search it returns the list of tasks that satisfty the condition.
53
+ The reason for supporting multiple searches is the UI that might be ending
54
+ needing periodic updates on task status for various searches at the same time.
55
+ This way, it is possible to get all the task statuses with one request.
56
+ DESC
57
+ def bulk_search
58
+ searches = Array(params[:searches])
59
+ @tasks = {}
60
+
61
+ ret = searches.map do |search_params|
62
+ { search_params: search_params,
63
+ results: search_tasks(search_params) }
64
+ end
65
+ render :json => ret
66
+ end
67
+
68
+ private
69
+
70
+ def search_tasks(search_params)
71
+ scope = ::ForemanTasks::Task.select('DISTINCT foreman_tasks_tasks.*')
72
+ scope = ordering_scope(scope, search_params)
73
+ scope = search_scope(scope, search_params)
74
+ scope = active_scope(scope, search_params)
75
+ scope = pagination_scope(scope, search_params)
76
+ scope.all.map { |task| task_hash(task) }
77
+ end
78
+
79
+ def search_scope(scope, search_params)
80
+ case search_params[:type]
81
+ when 'all'
82
+ scope
83
+ when 'user'
84
+ if search_params[:user_id].blank?
85
+ raise BadRequest, _("User search_params requires user_id to be specified")
86
+ end
87
+ scope.joins(:locks).where(foreman_tasks_locks:
88
+ { name: ::ForemanTasks::Lock::OWNER_LOCK_NAME,
89
+ resource_type: 'User',
90
+ resource_id: search_params[:user_id] })
91
+ when 'resource'
92
+ if search_params[:resource_type].blank? || search_params[:resource_id].blank?
93
+ raise BadRequest, _("Resource search_params requires resource_type and resource_id to be specified")
94
+ end
95
+ scope.joins(:locks).where(foreman_tasks_locks:
96
+ { resource_type: search_params[:resource_type],
97
+ resource_id: search_params[:resource_id] })
98
+ when 'task'
99
+ if search_params[:task_id].blank?
100
+ raise BadRequest, _("Task search_params requires task_id to be specified")
101
+ end
102
+ scope.where(id: search_params[:task_id])
103
+ else
104
+ raise BadRequest, _("Search_Params %s not supported") % search_params[:type]
105
+ end
106
+ end
107
+
108
+ def active_scope(scope, search_params)
109
+ if search_params[:active_only]
110
+ scope.active
111
+ else
112
+ scope
113
+ end
114
+ end
115
+
116
+ def pagination_scope(scope, search_params)
117
+ page = search_params[:page] || 1
118
+ per_page = search_params[:per_page] || 10
119
+ scope = scope.limit(per_page).offset((page - 1) * per_page)
120
+ end
121
+
122
+ def ordering_scope(scope, search_params)
123
+ scope.order('started_at DESC')
124
+ end
125
+
126
+ def task_hash(task)
127
+ return @tasks[task.id] if @tasks[task.id]
128
+ task_hash = Rabl.render(task, 'show', :view_path => "#{ForemanTasks::Engine.root}/app/views/foreman_tasks/api/tasks", :format => :hash, :scope => self)
129
+ @tasks[task.id] = task_hash
130
+ return task_hash
131
+ end
132
+
133
+ private
134
+
135
+ def find_task
136
+ @task = Task.find_by_id(params[:id])
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,26 @@
1
+ module ForemanTasks
2
+ module Concerns
3
+ module HostsControllerExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ alias_method_chain :facts, :dynflow
8
+ end
9
+
10
+ def facts_with_dynflow
11
+ task = ForemanTasks.async_task(::Actions::Foreman::Host::ImportFacts,
12
+ detect_host_type,
13
+ params[:name],
14
+ params[:facts],
15
+ params[:certname],
16
+ detected_proxy.try(:id))
17
+
18
+ render :json => {:task_id => task.id}, :status => 202
19
+ rescue ::Foreman::Exception => e
20
+ render :json => {'message'=>e.to_s}, :status => :unprocessable_entity
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,19 @@
1
+ module ForemanTasks
2
+ class TasksController < ::ApplicationController
3
+ include Foreman::Controller::AutoCompleteSearch
4
+
5
+ def show
6
+ @task = Task.find(params[:id])
7
+ end
8
+
9
+ def index
10
+ params[:order] ||= "started_at DESC"
11
+ @tasks = Task.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page])
12
+ end
13
+
14
+ # we need do this to make the Foreman helpers working properly
15
+ def controller_name
16
+ "foreman_tasks_tasks"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module ForemanTasks
2
+ module TasksHelper
3
+ def format_task_input(task, include_action = false)
4
+ parts = []
5
+ parts << task.humanized[:action] if include_action
6
+ parts << Array(task.humanized[:input]).map do |part|
7
+ if part.is_a? Array
8
+ part[1][:text]
9
+ else
10
+ part.to_s
11
+ end
12
+ end.join('; ')
13
+ h(parts.join(" "))
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ module Actions
2
+ class Base < Dynflow::Action
3
+
4
+ # This method says what data form input gets into the task details in Rest API
5
+ # By default, it sends the whole input there.
6
+ def task_input
7
+ self.input
8
+ end
9
+
10
+ # This method says what data form output gets into the task details in Rest API
11
+ # It should aggregate the important data that are worth to propagate to Rest API,
12
+ # perhaps also aggraget data from subactions if needed (using +all_actions+) method
13
+ # of Dynflow::Action::Presenter
14
+ def task_output
15
+ self.output
16
+ end
17
+
18
+ # This method should return humanized description of the action, e.g. "Install Package"
19
+ def humanized_name
20
+ action_class.name[/\w+$/].gsub(/([a-z])([A-Z])/) { "#{$1} #{$2}" }
21
+ end
22
+
23
+ # This method should return String of Array<String> describing input for the task
24
+ def humanized_input
25
+ task_input.pretty_inspect
26
+ end
27
+
28
+ # This method should return String describing output for the task.
29
+ # It should aggregate the data from subactions as well and it's used for humanized
30
+ # description of restuls of the action
31
+ def humanized_output
32
+ task_output.pretty_inspect
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ module Actions
2
+
3
+ class EntryAction < Actions::Base
4
+ include Helpers::ArgsSerialization
5
+ include Helpers::Lock
6
+
7
+ # what locks to use on the resource? All by default, can be overriden.
8
+ # It might one or more locks available for the resource. This following
9
+ # special values are supported as well:
10
+ #
11
+ # * `:all`: lock all possible operations (all locks defined in resource's
12
+ # `available_locks` method. Only tasks that link to the resource are
13
+ # allowed while running this task
14
+ # * `:exclusive`: same as `:all` + doesn't allow even linking to the resoruce.
15
+ # typical example is deleting a container, preventing all actions
16
+ # heppening on it's sub-resources (such a system).
17
+ def resource_locks
18
+ :all
19
+ end
20
+
21
+ # Peforms all that's needed to connect the action to the resource.
22
+ # It converts the resource (and it's relatives defined in +related_resources+
23
+ # to serialized form (using +to_action_input+).
24
+ #
25
+ # It also locks the resource on the actions defined in +resource_locks+ method.
26
+ #
27
+ # The additional args can include more resources and/or a hash
28
+ # with more data describing the action that should appear in the
29
+ # action's input.
30
+ def action_subject(resource, *additional_args)
31
+ if resource.respond_to?(:related_resources_recursive)
32
+ related_resources = resource.related_resources_recursive
33
+ else
34
+ related_resources = []
35
+ end
36
+ plan_self(serialize_args(resource, *related_resources, *additional_args))
37
+ if resource.is_a? ActiveRecord::Base
38
+ if resource_locks == :exclusive
39
+ exclusive_lock!(resource)
40
+ else
41
+ lock!(resource, resource_locks)
42
+ end
43
+ end
44
+ end
45
+
46
+ def humanized_input
47
+ Helpers::Humanizer.new(self).input
48
+ end
49
+
50
+ end
51
+ end