foreman-tasks 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|