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