foreman_remote_execution 0.0.1
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.
- checksums.yaml +15 -0
- data/LICENSE +619 -0
- data/README.md +54 -0
- data/Rakefile +47 -0
- data/app/assets/javascripts/template_input.js +9 -0
- data/app/assets/javascripts/template_invocation.js +32 -0
- data/app/assets/stylesheets/job_invocations.css.scss +35 -0
- data/app/assets/stylesheets/template_invocation.css.scss +16 -0
- data/app/controllers/api/v2/job_templates_controller.rb +108 -0
- data/app/controllers/job_invocations_controller.rb +35 -0
- data/app/controllers/job_templates_controller.rb +35 -0
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +40 -0
- data/app/helpers/remote_execution_helper.rb +88 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +93 -0
- data/app/lib/actions/remote_execution/run_hosts_job.rb +35 -0
- data/app/lib/actions/remote_execution/run_proxy_command.rb +34 -0
- data/app/models/concerns/foreman_remote_execution/bookmark_extensions.rb +9 -0
- data/app/models/concerns/foreman_remote_execution/foreman_tasks_task_extensions.rb +9 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +19 -0
- data/app/models/concerns/foreman_remote_execution/template_extensions.rb +20 -0
- data/app/models/concerns/foreman_remote_execution/template_relations.rb +10 -0
- data/app/models/concerns/foreman_remote_execution/user_extensions.rb +9 -0
- data/app/models/input_template_renderer.rb +42 -0
- data/app/models/job_invocation.rb +21 -0
- data/app/models/job_invocation_composer.rb +210 -0
- data/app/models/job_template.rb +52 -0
- data/app/models/remote_execution_provider.rb +17 -0
- data/app/models/setting/remote_execution.rb +19 -0
- data/app/models/ssh_execution_provider.rb +2 -0
- data/app/models/targeting.rb +56 -0
- data/app/models/targeting_host.rb +9 -0
- data/app/models/template_input.rb +154 -0
- data/app/models/template_invocation.rb +13 -0
- data/app/models/template_invocation_input_value.rb +8 -0
- data/app/views/api/v2/job_templates/base.json.rabl +3 -0
- data/app/views/api/v2/job_templates/create.json.rabl +3 -0
- data/app/views/api/v2/job_templates/index.json.rabl +3 -0
- data/app/views/api/v2/job_templates/main.json.rabl +5 -0
- data/app/views/api/v2/job_templates/show.json.rabl +9 -0
- data/app/views/job_invocations/_form.html.erb +67 -0
- data/app/views/job_invocations/_tab_hosts.html.erb +33 -0
- data/app/views/job_invocations/_tab_overview.html.erb +41 -0
- data/app/views/job_invocations/index.html.erb +30 -0
- data/app/views/job_invocations/new.html.erb +8 -0
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/app/views/job_invocations/show.html.erb +21 -0
- data/app/views/job_templates/_custom_tab_headers.html.erb +2 -0
- data/app/views/job_templates/_custom_tabs.html.erb +28 -0
- data/app/views/job_templates/auto_complete_job_name.json.erb +3 -0
- data/app/views/job_templates/edit.html.erb +6 -0
- data/app/views/job_templates/index.html.erb +33 -0
- data/app/views/job_templates/new.html.erb +6 -0
- data/app/views/template_inputs/_form.html.erb +22 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20150612121541_add_job_template_to_template.rb +6 -0
- data/db/migrate/20150616080015_create_template_input.rb +19 -0
- data/db/migrate/20150708133241_add_targeting.rb +25 -0
- data/db/migrate/20150708133242_add_invocation.rb +11 -0
- data/db/migrate/20150708133305_add_template_invocation.rb +22 -0
- data/db/migrate/20150812110800_add_resolved_at_to_targeting.rb +5 -0
- data/db/migrate/20150812145900_add_last_task_id_to_job_invocation.rb +6 -0
- data/db/seeds.d/60-ssh_proxy_feature.rb +2 -0
- data/lib/foreman_remote_execution/engine.rb +119 -0
- data/lib/foreman_remote_execution/version.rb +3 -0
- data/lib/foreman_remote_execution.rb +6 -0
- data/lib/tasks/foreman_remote_execution_tasks.rake +49 -0
- data/locale/Makefile +62 -0
- data/locale/en/foreman_remote_execution.po +19 -0
- data/locale/foreman_remote_execution.pot +19 -0
- data/locale/gemspec.rb +2 -0
- data/test/factories/foreman_remote_execution_factories.rb +48 -0
- data/test/functional/api/v2/job_templates_controller_test.rb +74 -0
- data/test/test_plugin_helper.rb +8 -0
- data/test/unit/actions/run_hosts_job_test.rb +40 -0
- data/test/unit/actions/run_proxy_command_test.rb +30 -0
- data/test/unit/input_template_renderer_test.rb +366 -0
- data/test/unit/job_invocation_composer_test.rb +415 -0
- data/test/unit/job_invocation_test.rb +31 -0
- data/test/unit/job_template_test.rb +5 -0
- data/test/unit/remote_execution_provider_test.rb +51 -0
- data/test/unit/targeting_test.rb +107 -0
- data/test/unit/template_input_test.rb +25 -0
- metadata +195 -0
data/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Foreman Remote Execution
|
|
2
|
+
|
|
3
|
+
A plugin bringing remote execution to the Foreman, completing the config
|
|
4
|
+
management functionality with remote management functionality.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
|
|
9
|
+
for how to install Foreman plugins
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
*Usage here*
|
|
14
|
+
|
|
15
|
+
## Generating the docs
|
|
16
|
+
|
|
17
|
+
0. ``cd doc``
|
|
18
|
+
|
|
19
|
+
0. ``mkdir .bin`` # only first time
|
|
20
|
+
|
|
21
|
+
1. ``bundle install``
|
|
22
|
+
|
|
23
|
+
2. ``bundle exec rake plantuml_install`` to install prerequisites
|
|
24
|
+
|
|
25
|
+
3. ``bundle exec jekyll serve`` to see the rendered changes locally
|
|
26
|
+
|
|
27
|
+
4. ``bundle exec rake publish`` to publish the changes to Github pages
|
|
28
|
+
|
|
29
|
+
## Links
|
|
30
|
+
|
|
31
|
+
* [the project page](http://theforeman.github.io/foreman_remote_execution/)
|
|
32
|
+
* [the issue tracker](http://projects.theforeman.org/projects/foreman_remote_execution)
|
|
33
|
+
|
|
34
|
+
## Contributing
|
|
35
|
+
|
|
36
|
+
Fork and send a Pull Request. Thanks!
|
|
37
|
+
|
|
38
|
+
## Copyright
|
|
39
|
+
|
|
40
|
+
Copyright (c) 2015 The Foreman developers
|
|
41
|
+
|
|
42
|
+
This program is free software: you can redistribute it and/or modify
|
|
43
|
+
it under the terms of the GNU General Public License as published by
|
|
44
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
45
|
+
(at your option) any later version.
|
|
46
|
+
|
|
47
|
+
This program is distributed in the hope that it will be useful,
|
|
48
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
49
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
50
|
+
GNU General Public License for more details.
|
|
51
|
+
|
|
52
|
+
You should have received a copy of the GNU General Public License
|
|
53
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
54
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
begin
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
rescue LoadError
|
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
6
|
+
end
|
|
7
|
+
begin
|
|
8
|
+
require 'rdoc/task'
|
|
9
|
+
rescue LoadError
|
|
10
|
+
require 'rdoc/rdoc'
|
|
11
|
+
require 'rake/rdoctask'
|
|
12
|
+
RDoc::Task = Rake::RDocTask
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
17
|
+
rdoc.title = 'ForemanRemoteExecution'
|
|
18
|
+
rdoc.options << '--line-numbers'
|
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
|
|
24
|
+
|
|
25
|
+
Bundler::GemHelper.install_tasks
|
|
26
|
+
|
|
27
|
+
require 'rake/testtask'
|
|
28
|
+
|
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
|
30
|
+
t.libs << 'lib'
|
|
31
|
+
t.libs << 'test'
|
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
|
33
|
+
t.verbose = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
task default: :test
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
require 'rubocop/rake_task'
|
|
40
|
+
RuboCop::RakeTask.new
|
|
41
|
+
rescue => _
|
|
42
|
+
puts 'Rubocop not loaded.'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
task :default do
|
|
46
|
+
Rake::Task['rubocop'].execute
|
|
47
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
$(document).on('change', 'select.input_type_selector', function () {
|
|
2
|
+
update_visibility_after_input_type_change($(this));
|
|
3
|
+
});
|
|
4
|
+
|
|
5
|
+
function update_visibility_after_input_type_change(select){
|
|
6
|
+
fieldset = select.closest('fieldset');
|
|
7
|
+
fieldset.find('div.custom_input_type_fields').hide();
|
|
8
|
+
fieldset.find('div.' + select.val() + '_input_type').show();
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
$(function() { job_invocation_form_binds() });
|
|
2
|
+
|
|
3
|
+
function refresh_execution_form() {
|
|
4
|
+
var form = $('form#job_invocation_form');
|
|
5
|
+
var data = form.serializeArray();
|
|
6
|
+
|
|
7
|
+
request = $.ajax({
|
|
8
|
+
data: data,
|
|
9
|
+
type: 'POST',
|
|
10
|
+
url: form.attr('data-refresh-url'),
|
|
11
|
+
dataType: 'script'
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
request.done(function () {
|
|
15
|
+
password_caps_lock_hint();
|
|
16
|
+
form.find('a[rel="popover-modal"]').popover({html: true});
|
|
17
|
+
form.find('select').select2({allowClear: true});
|
|
18
|
+
job_invocation_form_binds();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function job_invocation_form_binds() {
|
|
23
|
+
$('input.job_template_selector').on('click', function () {
|
|
24
|
+
parent_fieldset = $(this).closest('fieldset');
|
|
25
|
+
$(parent_fieldset).find('fieldset.job_template_form').hide();
|
|
26
|
+
$('#job_template_' + $(this).val()).show();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
$('select#job_invocation_job_name').on('change', refresh_execution_form);
|
|
30
|
+
|
|
31
|
+
$('button#refresh_execution_form').on('click', refresh_execution_form);
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
div#status_chart {
|
|
2
|
+
float: right;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
div.infoblock {
|
|
6
|
+
margin-bottom: 20px;
|
|
7
|
+
line-height: 2;
|
|
8
|
+
|
|
9
|
+
pre {
|
|
10
|
+
margin-top: 5px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
div.host_counter {
|
|
14
|
+
float: left;
|
|
15
|
+
min-width: 100px;
|
|
16
|
+
text-align: center;
|
|
17
|
+
|
|
18
|
+
.count {
|
|
19
|
+
font-size: 20px;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// chances are that these will be present in Foreman 1.10
|
|
25
|
+
.status-ok {
|
|
26
|
+
color: #5CB85C;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.status-error {
|
|
30
|
+
color: #D9534F
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.status-warn, .status-question {
|
|
34
|
+
color: #DB843D;
|
|
35
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
module V2
|
|
3
|
+
class JobTemplatesController < ::Api::V2::BaseController
|
|
4
|
+
include ::Api::Version2
|
|
5
|
+
include ::Api::TaxonomyScope
|
|
6
|
+
include ::Foreman::Renderer
|
|
7
|
+
include ::Foreman::Controller::ProvisioningTemplates
|
|
8
|
+
|
|
9
|
+
before_filter :find_optional_nested_object
|
|
10
|
+
before_filter :find_resource, :only => %w{show update destroy clone}
|
|
11
|
+
|
|
12
|
+
before_filter :handle_template_upload, :only => [:create, :update]
|
|
13
|
+
|
|
14
|
+
api :GET, "/job_templates/", N_("List job templates")
|
|
15
|
+
api :GET, "/locations/:location_id/job_templates/", N_("List job templates per location")
|
|
16
|
+
api :GET, "/organizations/:organization_id/job_templates/", N_("List job templates per organization")
|
|
17
|
+
param_group :taxonomy_scope, ::Api::V2::BaseController
|
|
18
|
+
param_group :search_and_pagination, ::Api::V2::BaseController
|
|
19
|
+
def index
|
|
20
|
+
@job_templates = resource_scope_for_index
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
api :GET, "/job_templates/:id", N_("Show job template details")
|
|
24
|
+
param :id, :identifier, :required => true
|
|
25
|
+
def show
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def_param_group :job_template do
|
|
29
|
+
param :job_template, Hash, :required => true, :action_aware => true do
|
|
30
|
+
param :name, String, :required => true, :desc => N_("Template name")
|
|
31
|
+
param :job_name, String, :required => true, :desc => N_("Job name")
|
|
32
|
+
param :template, String, :required => true
|
|
33
|
+
param :provider_type, String, :required => true, :desc => N_("Provider type")
|
|
34
|
+
param :snippet, :bool, :allow_nil => true
|
|
35
|
+
param :audit_comment, String, :allow_nil => true
|
|
36
|
+
param :locked, :bool, :desc => N_("Whether or not the template is locked for editing")
|
|
37
|
+
param_group :taxonomies, ::Api::V2::BaseController
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
api :POST, "/job_templates/", N_("Create a job template")
|
|
42
|
+
param_group :job_template, :as => :create
|
|
43
|
+
def create
|
|
44
|
+
@job_template = JobTemplate.new(params[:job_template])
|
|
45
|
+
process_response @job_template.save
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
api :PUT, "/job_templates/:id", N_("Update a job template")
|
|
49
|
+
param :id, :identifier, :required => true
|
|
50
|
+
param_group :job_template
|
|
51
|
+
def update
|
|
52
|
+
process_response @job_template.update_attributes(params[:job_template])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
api :GET, "/job_templates/revision"
|
|
56
|
+
param :version, String, :desc => N_("Template version")
|
|
57
|
+
def revision
|
|
58
|
+
audit = Audit.authorized(:view_audit_logs).find(params[:version])
|
|
59
|
+
render :json => audit.revision.template
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
api :DELETE, "/job_templates/:id", N_("Delete a job template")
|
|
63
|
+
param :id, :identifier, :required => true
|
|
64
|
+
def destroy
|
|
65
|
+
process_response @job_template.destroy
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def_param_group :job_template_clone do
|
|
69
|
+
param :job_template, Hash, :required => true, :action_aware => true do
|
|
70
|
+
param :name, String, :required => true, :desc => N_("Template name")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
api :POST, "/job_templates/:id/clone", N_("Clone a provision template")
|
|
75
|
+
param :id, :identifier, :required => true
|
|
76
|
+
param_group :job_template_clone, :as => :create
|
|
77
|
+
def clone
|
|
78
|
+
@job_template = @job_template.clone
|
|
79
|
+
load_vars_from_template
|
|
80
|
+
@job_template.name = params[:job_template][:name]
|
|
81
|
+
process_response @job_template.save
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resource_name
|
|
85
|
+
'job_template'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def action_permission
|
|
91
|
+
case params[:action]
|
|
92
|
+
when 'clone'
|
|
93
|
+
:create
|
|
94
|
+
else
|
|
95
|
+
super
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resource_class
|
|
100
|
+
JobTemplate
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def allowed_nested_id
|
|
104
|
+
%w(location_id organization_id)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class JobInvocationsController < ApplicationController
|
|
2
|
+
include Foreman::Controller::AutoCompleteSearch
|
|
3
|
+
|
|
4
|
+
def new
|
|
5
|
+
@composer = JobInvocationComposer.new(JobInvocation.new,
|
|
6
|
+
:host_ids => params[:host_ids],
|
|
7
|
+
:targeting => {
|
|
8
|
+
:targeting_type => Targeting::STATIC_TYPE,
|
|
9
|
+
:bookmark_id => params[:bookmark_id]
|
|
10
|
+
})
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
@composer = JobInvocationComposer.new(JobInvocation.new, params)
|
|
15
|
+
if @composer.save
|
|
16
|
+
@task = ForemanTasks.async_task(::Actions::RemoteExecution::RunHostsJob, @composer.job_invocation)
|
|
17
|
+
redirect_to job_invocation_path(@composer.job_invocation)
|
|
18
|
+
else
|
|
19
|
+
render :action => 'new'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show
|
|
24
|
+
@job_invocation = resource_base.find(params[:id])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def index
|
|
28
|
+
@job_invocations = resource_base.search_for(params[:search]).paginate(:page => params[:page]).order('id DESC')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# refreshes the form
|
|
32
|
+
def refresh
|
|
33
|
+
@composer = JobInvocationComposer.new(JobInvocation.new, params)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class JobTemplatesController < ::TemplatesController
|
|
2
|
+
def load_vars_from_template
|
|
3
|
+
return unless @template
|
|
4
|
+
|
|
5
|
+
@locations = @template.locations
|
|
6
|
+
@organizations = @template.organizations
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def auto_complete_job_name
|
|
10
|
+
@job_names = resource_base.where(['job_name LIKE ?', "%#{params[:search]}%"]).pluck(:job_name).uniq
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def preview
|
|
14
|
+
base = Host.authorized(:view_hosts)
|
|
15
|
+
host = params[:preview_host_id].present? ? base.find(params[:preview_host_id]) : base.first
|
|
16
|
+
@template.template = params[:template]
|
|
17
|
+
renderer = InputTemplateRenderer.new(@template, host)
|
|
18
|
+
if (output = renderer.preview)
|
|
19
|
+
render :text => output
|
|
20
|
+
else
|
|
21
|
+
render :status => 406, :text => renderer.error_message
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def action_permission
|
|
28
|
+
case params[:action]
|
|
29
|
+
when 'auto_complete_job_name'
|
|
30
|
+
:view_job_templates
|
|
31
|
+
else
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module ForemanRemoteExecution
|
|
2
|
+
module HostsHelperExtensions
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
alias_method_chain(:host_title_actions, :run_button)
|
|
7
|
+
alias_method_chain :multiple_actions, :remote_execution
|
|
8
|
+
alias_method_chain :multiple_actions_select, :remote_execution
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def multiple_actions_with_remote_execution
|
|
12
|
+
# The modal window loads empty page, we hook the custom function
|
|
13
|
+
# to the link later. The ?run_job=true is there just to be able to identify the link
|
|
14
|
+
multiple_actions_without_remote_execution + [[_('Run Job'), 'blank.html?run_job=true']]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def host_title_actions_with_run_button(*args)
|
|
18
|
+
title_actions(button_group(link_to(_("Run Job"), new_job_invocation_path(:host_ids => [args.first.id]), :id => :run_button)))
|
|
19
|
+
host_title_actions_without_run_button(*args)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def multiple_actions_select_with_remote_execution(*args)
|
|
23
|
+
# TODO: awful hack to open the job invocation form as a new
|
|
24
|
+
# page rather than using the AJAX inside a modal window.
|
|
25
|
+
# Since we want for this plugin to be compatible with 1.9, we
|
|
26
|
+
# need to monkey patch/js hack for now, but it should be removed
|
|
27
|
+
# after we fix and release http://projects.theforeman.org/issues/11309
|
|
28
|
+
multiple_actions_select_without_remote_execution(*args) +
|
|
29
|
+
<<-JAVASCRIPT.html_safe
|
|
30
|
+
<script>
|
|
31
|
+
$(function () {
|
|
32
|
+
$("#submit_multiple a[onclick*='blank.html?run_job=true']").click(function (e) {
|
|
33
|
+
document.location = "#{new_job_invocation_path}?" + $.param({host_ids: $.foremanSelectedHosts});
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
JAVASCRIPT
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module RemoteExecutionHelper
|
|
2
|
+
def providers_options
|
|
3
|
+
RemoteExecutionProvider.providers.map { |key, provider| [ key, _(provider) ] }
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def template_input_types_options
|
|
7
|
+
TemplateInput::TYPES.map { |key, name| [ _(name), key ] }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def job_invocation_chart(invocation)
|
|
11
|
+
options = { :class => 'statistics-pie small', :expandable => true, :border => 0, :show_title => true }
|
|
12
|
+
|
|
13
|
+
if (bulk_task = invocation.last_task)
|
|
14
|
+
success = bulk_task.output['success_count'] || 0
|
|
15
|
+
failed = bulk_task.output['failed_count'] || 0
|
|
16
|
+
pending = (bulk_task.output['pending_count'] || 0)
|
|
17
|
+
|
|
18
|
+
flot_pie_chart('status', job_invocation_status(invocation),
|
|
19
|
+
[{:label => _('Success'), :data => success, :color => '#5CB85C'},
|
|
20
|
+
{:label => _('Failed'), :data => failed, :color => '#D9534F'},
|
|
21
|
+
{:label => _('Pending'), :data => pending, :color => '#DEDEDE'}],
|
|
22
|
+
options)
|
|
23
|
+
else
|
|
24
|
+
content_tag(:h4, job_invocation_status(invocation))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def job_invocation_status(invocation)
|
|
29
|
+
if invocation.last_task.blank?
|
|
30
|
+
_('Job not started yet 0%')
|
|
31
|
+
else
|
|
32
|
+
label = invocation.last_task.pending ? _('Running') : _('Finished')
|
|
33
|
+
label + ' ' + (invocation.last_task.progress * 100).to_i.to_s + '%'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def host_counter(label, count)
|
|
39
|
+
content_tag(:div, :class => 'host_counter') do
|
|
40
|
+
content_tag(:div, label, :class => 'header') + content_tag(:div, count.to_s, :class => 'count')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def template_invocation_status(task)
|
|
45
|
+
case task.result
|
|
46
|
+
when 'warning', 'error'
|
|
47
|
+
content_tag(:i, ' '.html_safe, :class => 'glyphicon glyphicon-exclamation-sign') + content_tag(:span, _('failed'), :class => 'status-error')
|
|
48
|
+
when 'success'
|
|
49
|
+
content_tag(:i, ' '.html_safe, :class => 'glyphicon glyphicon-ok-sign') + content_tag(:span, _('success'), :class => 'status-ok')
|
|
50
|
+
when 'pending'
|
|
51
|
+
content_tag(:i, ' '.html_safe, :class => 'glyphicon glyphicon-question-sign') + content_tag(:span, _('pending'))
|
|
52
|
+
else
|
|
53
|
+
task.result
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def job_invocation_task_buttons(task)
|
|
58
|
+
buttons = []
|
|
59
|
+
buttons << link_to(_('Refresh'), {}, :class => 'btn btn-default', :title => _('Refresh this page'))
|
|
60
|
+
if authorized_for(:permission => :view_foreman_tasks, :auth_object => task)
|
|
61
|
+
buttons << link_to(_("Last Job Task"), foreman_tasks_task_path(task),
|
|
62
|
+
:class => "btn btn-default",
|
|
63
|
+
:title => _('See the last task details'))
|
|
64
|
+
end
|
|
65
|
+
if authorized_for(:permission => :edit_foreman_tasks, :auth_object => task)
|
|
66
|
+
buttons << link_to(_("Cancel Job"), cancel_foreman_tasks_task_path(task),
|
|
67
|
+
:class => "btn btn-danger",
|
|
68
|
+
:title => _('Try to cancel the job'),
|
|
69
|
+
:disabled => !task.cancellable?,
|
|
70
|
+
:method => :post)
|
|
71
|
+
end
|
|
72
|
+
return button_group(*buttons)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def link_to_invocation_task_if_authorized(invocation)
|
|
76
|
+
if invocation.last_task.present?
|
|
77
|
+
link_to_if_authorized job_invocation_status(invocation),
|
|
78
|
+
hash_for_foreman_tasks_task_path(invocation.last_task).merge(:auth_object => invocation.last_task, :permission => :view_foreman_tasks)
|
|
79
|
+
else
|
|
80
|
+
job_invocation_status(invocation)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def invocation_count(invocation, options = {})
|
|
85
|
+
options = { :unknown_string => 'N/A' }.merge(options)
|
|
86
|
+
(invocation.last_task.try(:output) || {}).fetch(options[:output_key], options[:unknown_string])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Actions
|
|
2
|
+
module RemoteExecution
|
|
3
|
+
class RunHostJob < Actions::EntryAction
|
|
4
|
+
|
|
5
|
+
def resource_locks
|
|
6
|
+
:link
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
include ::Dynflow::Action::Cancellable
|
|
10
|
+
|
|
11
|
+
def plan(job_invocation, host)
|
|
12
|
+
action_subject(host, :job_name => job_invocation.job_name)
|
|
13
|
+
|
|
14
|
+
template_invocation = find_template_invocation(job_invocation, host)
|
|
15
|
+
hostname = find_ip_or_hostname(host)
|
|
16
|
+
proxy = find_proxy(template_invocation, host)
|
|
17
|
+
|
|
18
|
+
renderer = InputTemplateRenderer.new(template_invocation.template, host, template_invocation)
|
|
19
|
+
script = renderer.render
|
|
20
|
+
raise _("Failed rendering template: %s") % renderer.error_message unless script
|
|
21
|
+
|
|
22
|
+
link!(job_invocation)
|
|
23
|
+
link!(template_invocation)
|
|
24
|
+
|
|
25
|
+
plan_action(RunProxyCommand, proxy, hostname, script)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def humanized_output
|
|
29
|
+
host_run_action = planned_actions(RunProxyCommand).first
|
|
30
|
+
proxy_output = host_run_action && host_run_action.output[:proxy_output]
|
|
31
|
+
return unless proxy_output
|
|
32
|
+
output = []
|
|
33
|
+
if proxy_output[:result]
|
|
34
|
+
output << proxy_output[:result].map { |o| o[:output] }.join("")
|
|
35
|
+
end
|
|
36
|
+
output << "Exit status: #{host_run_action.exit_status}" if host_run_action.exit_status
|
|
37
|
+
return output.join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def humanized_name
|
|
41
|
+
_('Run %{job_name} on %{host}') % { :job_name => input[:job_name], :host => input[:host][:name] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_template_invocation(job_invocation, host)
|
|
45
|
+
providers = available_providers(job_invocation, host)
|
|
46
|
+
providers.each do |provider|
|
|
47
|
+
job_invocation.template_invocations.each do |template_invocation|
|
|
48
|
+
if template_invocation.template.provider_type == provider
|
|
49
|
+
return template_invocation
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
raise _("Could not use any template used in the job invocation")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find_ip_or_hostname(host)
|
|
58
|
+
host.interfaces.each do |interface|
|
|
59
|
+
return interface.ip unless interface.ip.blank?
|
|
60
|
+
end
|
|
61
|
+
return host.name
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def available_providers(job_invocation, host)
|
|
65
|
+
# TODO: determine from the host and job_invocation details
|
|
66
|
+
return ['Ssh']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_proxy(template_invocation, host)
|
|
70
|
+
provider = template_invocation.template.provider_type.to_s
|
|
71
|
+
all_host_proxies(host).each do |proxies|
|
|
72
|
+
if proxy = proxies.joins(:features).where("features.name = ?", provider).first
|
|
73
|
+
return proxy
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
raise _("Could not use any proxy: assign a proxy with provider '%{provider}' to the host or set '%{global_proxy_setting}' in settings") %\
|
|
77
|
+
{ :provider => provider, :global_proxy_setting => 'remote_execution_global_proxy' }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def all_host_proxies(host)
|
|
81
|
+
Enumerator.new do |e|
|
|
82
|
+
host.interfaces.each do |interface|
|
|
83
|
+
if interface.subnet
|
|
84
|
+
e << ::SmartProxy.where(:id => interface.subnet.proxies.map(&:id))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
e << host.smart_proxies
|
|
88
|
+
e << ::SmartProxy.authorized if Setting[:remote_execution_global_proxy]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Actions
|
|
2
|
+
module RemoteExecution
|
|
3
|
+
class RunHostsJob < Actions::ActionWithSubPlans
|
|
4
|
+
def plan(job_invocation)
|
|
5
|
+
action_subject(job_invocation)
|
|
6
|
+
|
|
7
|
+
job_invocation.targeting.resolve_hosts!
|
|
8
|
+
job_invocation.update_attribute :last_task_id, task.id
|
|
9
|
+
input.update(:job_name => job_invocation.job_name)
|
|
10
|
+
plan_self(job_invocation_id: job_invocation.id)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_sub_plans
|
|
14
|
+
job_invocation = JobInvocation.find(input[:job_invocation_id])
|
|
15
|
+
job_invocation.targeting.hosts.map do |host|
|
|
16
|
+
trigger(RunHostJob, job_invocation, host)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rescue_strategy
|
|
21
|
+
::Dynflow::Action::Rescue::Skip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run(event = nil)
|
|
25
|
+
super unless event == Dynflow::Action::Skip
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def output
|
|
29
|
+
result = super
|
|
30
|
+
result[:pending_count] = (result[:total_count] || 0) - (result[:failed_count] || 0) - (result[:success_count] || 0)
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Actions
|
|
2
|
+
module RemoteExecution
|
|
3
|
+
class RunProxyCommand < Actions::ProxyAction
|
|
4
|
+
|
|
5
|
+
include ::Dynflow::Action::Cancellable
|
|
6
|
+
|
|
7
|
+
def plan(proxy, hostname, script, options = {})
|
|
8
|
+
options = { :effective_user => nil }.merge(options)
|
|
9
|
+
super(proxy, options.merge(:hostname => hostname, :script => script))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def proxy_action_name
|
|
13
|
+
'Proxy::RemoteExecution::Ssh::CommandAction'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def on_data(data)
|
|
17
|
+
super(data)
|
|
18
|
+
error! _("Script execution failed") if failed_run?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rescue_strategy
|
|
22
|
+
::Dynflow::Action::Rescue::Skip
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def failed_run?
|
|
26
|
+
exit_status && proxy_output[:exit_status] != 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def exit_status
|
|
30
|
+
proxy_output && proxy_output[:exit_status]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|