foreman_remote_execution 0.0.10 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/README.md +14 -21
- data/app/assets/javascripts/template_invocation.js +29 -0
- data/app/controllers/job_invocations_controller.rb +13 -0
- data/app/controllers/job_templates_controller.rb +1 -1
- data/app/helpers/remote_execution_helper.rb +11 -4
- data/app/lib/actions/remote_execution/run_host_job.rb +19 -3
- data/app/lib/proxy_api/remote_execution_ssh.rb +14 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +27 -1
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +26 -0
- data/app/models/host_status/execution_status.rb +49 -0
- data/app/models/job_invocation_composer.rb +10 -2
- data/app/models/job_template.rb +1 -0
- data/app/models/setting/remote_execution.rb +4 -1
- data/app/models/targeting.rb +5 -3
- data/app/views/job_invocations/_form.html.erb +4 -1
- data/app/views/job_invocations/_preview_hosts_list.html.erb +19 -0
- data/app/views/job_invocations/_preview_hosts_modal.html.erb +13 -0
- data/app/views/job_invocations/new.html.erb +1 -5
- data/app/views/job_invocations/show.js.erb +1 -0
- data/app/views/unattended/snippets/_remote_execution_ssh_keys.erb +18 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20151013135415_add_pub_key_to_smart_proxy.rb +5 -0
- data/db/seeds.d/80-provision_templates.rb +21 -0
- data/lib/foreman_remote_execution/engine.rb +7 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +2 -2
- data/test/unit/concerns/host_extensions_test.rb +29 -0
- data/test/unit/job_invocation_composer_test.rb +8 -3
- data/test/unit/job_template_test.rb +13 -0
- data/test/unit/targeting_test.rb +1 -1
- metadata +26 -19
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZGY2YjZkZGY4MzliZmMxMzg5Y2U2Yjg0NGRiNzQ5ZjZkZjA4YmIzYg==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a83d62de427a930126dbdac52051e2a6ca6e0cc
|
4
|
+
data.tar.gz: a304bc71914618eb42fa1648b4f6ce3f2f31fceb
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
ZTgyOGFlYzc1ZDA5NzVmNzM1MmM5NjQ3YWU2ZmZjZDA2YTE0N2ZmNDQwMzNj
|
11
|
-
OGRiMmUxOTVjNzMwOTNhZDIwNGFkZjdiZWM5NjI0MWI5YjNiYTY=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
YzgyYzRmYTExZDlmNDczZGExMWQzN2UyM2FlMTYzY2RmODIyMmI3MjNkZWJj
|
14
|
-
OTY4YTIyNzg2ODI3NDgzYjk2ODAwNzcwMmYxNjAwMmQwMDlmMWFmNGUzOGM0
|
15
|
-
MDY1YjdkZjY2OWE3MjcwYzBlNTU2ZTNjMGE3MWZjN2VlMzNiYmQ=
|
6
|
+
metadata.gz: 9afcf6f187db3707ed4842ad877d8273c3bead70b25f0fb078dacc2888d0218cbed4d41c2b2e85e874cc88522946488915ff123883e12f507638c9ef1581f9ea
|
7
|
+
data.tar.gz: 8a38221109dea89d1b5c91f84179727e563b2f880a948b83029adda2df3b6ffddd058dcc5e5651cb32d06bc1c6ada3bc6ddcc6b15e83fb8c3f9db71296c8d371
|
data/README.md
CHANGED
@@ -5,36 +5,29 @@
|
|
5
5
|
|
6
6
|
# Foreman Remote Execution
|
7
7
|
|
8
|
-
A plugin bringing remote execution to the Foreman, completing the config
|
8
|
+
A plugin bringing remote execution to the Foreman, completing the config
|
9
9
|
management functionality with remote management functionality.
|
10
10
|
|
11
|
-
|
11
|
+
* Website: [theforeman.org](http://theforeman.org)
|
12
|
+
* Support: [Foreman support](http://theforeman.org/support.html)
|
12
13
|
|
13
|
-
|
14
|
-
for how to install Foreman plugins
|
14
|
+
## Features
|
15
15
|
|
16
|
-
|
16
|
+
* Visualize remote execution job process live
|
17
|
+
![job detail](http://theforeman.org/plugins/foreman_remote_execution/0.0/job_detail_1.png)
|
18
|
+
* Schedule or run jobs on hosts
|
19
|
+
![invocation form](http://theforeman.org/plugins/foreman_remote_execution/0.0/invocation_form.png)
|
20
|
+
* Create templates to customize your jobs
|
21
|
+
![job templates](http://theforeman.org/plugins/foreman_remote_execution/0.0/job_template_form.png)
|
17
22
|
|
18
|
-
|
23
|
+
## Installation and usage
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
0. ``cd doc``
|
23
|
-
|
24
|
-
0. ``mkdir .bin`` # only first time
|
25
|
-
|
26
|
-
1. ``bundle install``
|
27
|
-
|
28
|
-
2. ``bundle exec rake plantuml_install`` to install prerequisites
|
29
|
-
|
30
|
-
3. ``bundle exec jekyll serve`` to see the rendered changes locally
|
31
|
-
|
32
|
-
4. ``bundle exec rake publish`` to publish the changes to Github pages
|
25
|
+
Check the Foreman manual [remote execution section](http://theforeman.org/plugins/foreman_remote_execution/)
|
33
26
|
|
34
27
|
## Links
|
35
28
|
|
36
|
-
* [
|
37
|
-
* [
|
29
|
+
* [Design document](http://theforeman.github.io/foreman_remote_execution/design/)
|
30
|
+
* [Issue tracker](http://projects.theforeman.org/projects/foreman_remote_execution)
|
38
31
|
|
39
32
|
## Contributing
|
40
33
|
|
@@ -24,6 +24,33 @@ function refresh_search_query(value){
|
|
24
24
|
$('textarea#targeting_search_query').val($('span#bookmark_query_map span#bookmark-' + id).data('query'));
|
25
25
|
}
|
26
26
|
|
27
|
+
function show_preview_hosts_modal() {
|
28
|
+
var modal_window = $('#previewHostsModal');
|
29
|
+
|
30
|
+
var form = $('form#job_invocation_form');
|
31
|
+
var data = form.serializeArray();
|
32
|
+
|
33
|
+
request = $.ajax({
|
34
|
+
data: data,
|
35
|
+
type: 'GET',
|
36
|
+
url: modal_window.attr('data-url'),
|
37
|
+
success: function(request) {
|
38
|
+
modal_window.find('.modal-body').html(request);
|
39
|
+
},
|
40
|
+
complete: function() {
|
41
|
+
modal_window.modal({'show': true});
|
42
|
+
modal_window.find('a[rel="popover-modal"]').popover();
|
43
|
+
}
|
44
|
+
});
|
45
|
+
}
|
46
|
+
|
47
|
+
function close_preview_hosts_modal() {
|
48
|
+
var modal_window = $('#previewHostsModal');
|
49
|
+
modal_window.modal('hide');
|
50
|
+
modal_window.removeData();
|
51
|
+
modal_window.find('.modal-body').html('');
|
52
|
+
}
|
53
|
+
|
27
54
|
function job_invocation_form_binds() {
|
28
55
|
$('input.job_template_selector').on('click', function () {
|
29
56
|
parent_fieldset = $(this).closest('fieldset');
|
@@ -40,6 +67,8 @@ function job_invocation_form_binds() {
|
|
40
67
|
|
41
68
|
$('button#refresh_execution_form').on('click', refresh_execution_form);
|
42
69
|
|
70
|
+
$('button#preview_hosts').on('click', show_preview_hosts_modal);
|
71
|
+
|
43
72
|
$('textarea#targeting_search_query').on('change', refresh_execution_form);
|
44
73
|
|
45
74
|
$('select#targeting_bookmark_id').on('change', refresh_search_query);
|
@@ -56,12 +56,25 @@ class JobInvocationsController < ApplicationController
|
|
56
56
|
@composer = JobInvocationComposer.new.compose_from_params(params)
|
57
57
|
end
|
58
58
|
|
59
|
+
def preview_hosts
|
60
|
+
composer = JobInvocationComposer.new.compose_from_params(params)
|
61
|
+
|
62
|
+
@hosts = composer.targeted_hosts.limit(Setting[:entries_per_page])
|
63
|
+
@additional = composer.targeted_hosts.count - Setting[:entries_per_page]
|
64
|
+
@dynamic = composer.targeting.dynamic?
|
65
|
+
@query = composer.displayed_search_query
|
66
|
+
|
67
|
+
render :partial => 'job_invocations/preview_hosts_list'
|
68
|
+
end
|
69
|
+
|
59
70
|
private
|
60
71
|
|
61
72
|
def action_permission
|
62
73
|
case params[:action]
|
63
74
|
when 'rerun'
|
64
75
|
'create'
|
76
|
+
when 'preview_hosts'
|
77
|
+
'create'
|
65
78
|
else
|
66
79
|
super
|
67
80
|
end
|
@@ -11,7 +11,7 @@ class JobTemplatesController < ::TemplatesController
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def preview
|
14
|
-
base = Host.authorized(:view_hosts)
|
14
|
+
base = Host.authorized(:view_hosts, Host)
|
15
15
|
host = params[:preview_host_id].present? ? base.find(params[:preview_host_id]) : base.first
|
16
16
|
@template.template = params[:template]
|
17
17
|
renderer = InputTemplateRenderer.new(@template, host)
|
@@ -11,7 +11,7 @@ module RemoteExecutionHelper
|
|
11
11
|
options = { :class => 'statistics-pie small', :expandable => true, :border => 0, :show_title => true }
|
12
12
|
|
13
13
|
if (bulk_task = invocation.last_task)
|
14
|
-
failed_tasks = bulk_task.sub_tasks.select { |sub_task|
|
14
|
+
failed_tasks = bulk_task.sub_tasks.select { |sub_task| task_failed? sub_task }
|
15
15
|
cancelled_tasks, failed_tasks = failed_tasks.partition { |task| task_cancelled? task }
|
16
16
|
success = bulk_task.output['success_count'] || 0
|
17
17
|
cancelled = cancelled_tasks.length
|
@@ -42,6 +42,10 @@ module RemoteExecutionHelper
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
+
def task_failed?(task)
|
46
|
+
%w(warning error).include? task.result
|
47
|
+
end
|
48
|
+
|
45
49
|
def task_cancelled?(task)
|
46
50
|
task.execution_plan.errors.map(&:exception).any? { |exception| exception.class == ::ForemanTasks::Task::TaskCancelledException }
|
47
51
|
end
|
@@ -77,7 +81,7 @@ module RemoteExecutionHelper
|
|
77
81
|
if task.nil?
|
78
82
|
[]
|
79
83
|
else
|
80
|
-
[display_link_if_authorized(_("Details"), hash_for_template_invocation_path(:id => task).merge(:auth_object => host, :permission => :
|
84
|
+
[display_link_if_authorized(_("Details"), hash_for_template_invocation_path(:id => task).merge(:auth_object => host, :permission => :view_hosts))]
|
81
85
|
end
|
82
86
|
end
|
83
87
|
|
@@ -86,17 +90,19 @@ module RemoteExecutionHelper
|
|
86
90
|
template_invocation.nil? ? _('N/A') : _(RemoteExecutionProvider.provider_for(template_invocation.template.provider_type))
|
87
91
|
end
|
88
92
|
|
93
|
+
# rubocop:disable Metrics/AbcSize
|
89
94
|
def job_invocation_task_buttons(task)
|
90
95
|
buttons = []
|
91
96
|
buttons << link_to(_('Refresh'), {}, :class => 'btn btn-default', :title => _('Refresh this page'))
|
92
|
-
if authorized_for(
|
97
|
+
if authorized_for(hash_for_new_job_invocation_path)
|
93
98
|
buttons << link_to(_("Rerun"), rerun_job_invocation_path(:id => task.locks.where(:resource_type => 'JobInvocation').first.resource),
|
94
99
|
:class => "btn btn-default",
|
95
100
|
:title => _('Rerun the job'))
|
96
101
|
end
|
97
|
-
if authorized_for(
|
102
|
+
if authorized_for(hash_for_new_job_invocation_path)
|
98
103
|
buttons << link_to(_("Rerun failed"), rerun_job_invocation_path(:id => task.locks.where(:resource_type => 'JobInvocation').first.resource, :failed_only => 1),
|
99
104
|
:class => "btn btn-default",
|
105
|
+
:disabled => !task.sub_tasks.any? { |sub_task| task_failed?(sub_task) },
|
100
106
|
:title => _('Rerun on failed hosts'))
|
101
107
|
end
|
102
108
|
if authorized_for(:permission => :view_foreman_tasks, :auth_object => task)
|
@@ -113,6 +119,7 @@ module RemoteExecutionHelper
|
|
113
119
|
end
|
114
120
|
return buttons
|
115
121
|
end
|
122
|
+
# rubocop:enable Metrics/AbcSize
|
116
123
|
|
117
124
|
def template_invocation_task_buttons(task)
|
118
125
|
buttons = []
|
@@ -6,8 +6,6 @@ module Actions
|
|
6
6
|
:link
|
7
7
|
end
|
8
8
|
|
9
|
-
include ::Dynflow::Action::Cancellable
|
10
|
-
|
11
9
|
def plan(job_invocation, host, template_invocation, proxy, connection_options = {})
|
12
10
|
action_subject(host, :job_name => job_invocation.job_name)
|
13
11
|
hostname = find_ip_or_hostname(host)
|
@@ -27,7 +25,16 @@ module Actions
|
|
27
25
|
link!(job_invocation)
|
28
26
|
link!(template_invocation)
|
29
27
|
|
30
|
-
|
28
|
+
provider = template_invocation.template.provider_type.to_s
|
29
|
+
plan_action(RunProxyCommand, proxy, hostname, script, { :connection_options => connection_options }.merge(provider_settings(provider, host)))
|
30
|
+
plan_self
|
31
|
+
end
|
32
|
+
|
33
|
+
def finalize(*args)
|
34
|
+
host = Host.find(input[:host][:id])
|
35
|
+
host.refresh_statuses
|
36
|
+
rescue => e
|
37
|
+
Foreman::Logging.exception "Could not update execution status for #{input[:host][:name]}", e
|
31
38
|
end
|
32
39
|
|
33
40
|
def humanized_output
|
@@ -55,6 +62,15 @@ module Actions
|
|
55
62
|
|
56
63
|
return host.fqdn
|
57
64
|
end
|
65
|
+
|
66
|
+
def provider_settings(provider, host)
|
67
|
+
case provider
|
68
|
+
when 'Ssh'
|
69
|
+
{ :ssh_user => host.params['remote_execution_ssh_user'] || Setting[:remote_execution_ssh_user] }
|
70
|
+
else
|
71
|
+
{}
|
72
|
+
end
|
73
|
+
end
|
58
74
|
end
|
59
75
|
end
|
60
76
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ::ProxyAPI
|
2
|
+
class RemoteExecutionSSH < ::ProxyAPI::Resource
|
3
|
+
def initialize(args)
|
4
|
+
@url = args[:url] + '/ssh'
|
5
|
+
super args
|
6
|
+
end
|
7
|
+
|
8
|
+
def pubkey
|
9
|
+
get('pubkey').strip
|
10
|
+
rescue => e
|
11
|
+
raise ProxyException.new(url, e, N_('Unable to fetch public key'))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -6,8 +6,30 @@ module ForemanRemoteExecution
|
|
6
6
|
alias_method_chain :build_required_interfaces, :remote_execution
|
7
7
|
alias_method_chain :reload, :remote_execution
|
8
8
|
alias_method_chain :becomes, :remote_execution
|
9
|
+
alias_method_chain :params, :remote_execution
|
9
10
|
|
10
11
|
has_many :targeting_hosts, :dependent => :destroy, :foreign_key => 'host_id'
|
12
|
+
|
13
|
+
has_one :execution_status_object, :class_name => 'HostStatus::ExecutionStatus', :foreign_key => 'host_id'
|
14
|
+
|
15
|
+
scoped_search :in => :execution_status_object, :on => :status, :rename => :'execution_status',
|
16
|
+
:complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
|
17
|
+
end
|
18
|
+
|
19
|
+
def execution_status(options = {})
|
20
|
+
@execution_status ||= get_status(HostStatus::ExecutionStatus).to_status(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def execution_status_label(options = {})
|
24
|
+
@execution_status_label ||= get_status(HostStatus::ExecutionStatus).to_label(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def params_with_remote_execution
|
28
|
+
params = params_without_remote_execution
|
29
|
+
keys = remote_execution_ssh_keys
|
30
|
+
params['remote_execution_ssh_keys'] = keys unless keys.blank?
|
31
|
+
params['remote_execution_ssh_user'] = Setting[:remote_execution_ssh_user] unless params.key?('remote_execution_ssh_user')
|
32
|
+
params
|
11
33
|
end
|
12
34
|
|
13
35
|
def execution_interface
|
@@ -32,13 +54,17 @@ module ForemanRemoteExecution
|
|
32
54
|
proxies
|
33
55
|
end
|
34
56
|
|
57
|
+
def remote_execution_ssh_keys
|
58
|
+
remote_execution_proxies('Ssh').values.flatten.uniq.map { |proxy| proxy.pubkey }.compact.uniq
|
59
|
+
end
|
60
|
+
|
35
61
|
def drop_execution_interface_cache
|
36
62
|
@execution_interface = nil
|
37
63
|
end
|
38
64
|
|
39
65
|
def becomes_with_remote_execution(*args)
|
40
66
|
became = becomes_without_remote_execution(*args)
|
41
|
-
became.drop_execution_interface_cache
|
67
|
+
became.drop_execution_interface_cache if became.respond_to? :drop_execution_interface_cache
|
42
68
|
became
|
43
69
|
end
|
44
70
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ForemanRemoteExecution
|
2
|
+
module SmartProxyExtensions
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
alias_method_chain :refresh, :remote_execution
|
7
|
+
end
|
8
|
+
|
9
|
+
def pubkey
|
10
|
+
self[:pubkey] || update_pubkey
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_pubkey
|
14
|
+
return unless has_feature?('Ssh')
|
15
|
+
key = ::ProxyAPI::RemoteExecutionSSH.new(:url => url).pubkey
|
16
|
+
self.update_attribute(:pubkey, key) if key
|
17
|
+
key
|
18
|
+
end
|
19
|
+
|
20
|
+
def refresh_with_remote_execution
|
21
|
+
errors = refresh_without_remote_execution
|
22
|
+
update_pubkey
|
23
|
+
errors
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class HostStatus::ExecutionStatus < HostStatus::Status
|
2
|
+
OK = 0
|
3
|
+
ERROR = 1
|
4
|
+
|
5
|
+
def relevant?
|
6
|
+
execution_tasks.present?
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_status(options = {})
|
10
|
+
if last_stopped_task.nil? || last_stopped_task.result == 'success'
|
11
|
+
OK
|
12
|
+
else
|
13
|
+
ERROR
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_global(options = {})
|
18
|
+
if to_status(options) == ERROR
|
19
|
+
return HostStatus::Global::ERROR
|
20
|
+
else
|
21
|
+
return HostStatus::Global::OK
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.status_name
|
26
|
+
N_('Execution')
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_label(options = {})
|
30
|
+
case to_status(options)
|
31
|
+
when OK
|
32
|
+
execution_tasks.present? ? N_('Last execution succeeded') : N_('No execution finished yet')
|
33
|
+
when ERROR
|
34
|
+
N_('Last execution failed')
|
35
|
+
else
|
36
|
+
N_('Unknown execution status')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def last_stopped_task
|
43
|
+
@last_stopped_task ||= execution_tasks.order(:started_at).where(:state => 'stopped').last
|
44
|
+
end
|
45
|
+
|
46
|
+
def execution_tasks
|
47
|
+
ForemanTasks::Task::DynflowTask.for_action(Actions::RemoteExecution::RunHostJob).for_resource(host)
|
48
|
+
end
|
49
|
+
end
|
@@ -118,8 +118,16 @@ class JobInvocationComposer
|
|
118
118
|
Bookmark.authorized(:view_bookmarks).my_bookmarks.where(:controller => ['hosts', 'dashboard'])
|
119
119
|
end
|
120
120
|
|
121
|
+
def targeted_hosts
|
122
|
+
if displayed_search_query.blank?
|
123
|
+
Host.where('1 = 0')
|
124
|
+
else
|
125
|
+
Host.authorized(Targeting::RESOLVE_PERMISSION, Host).search_for(displayed_search_query)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
121
129
|
def targeted_hosts_count
|
122
|
-
|
130
|
+
targeted_hosts.count
|
123
131
|
rescue
|
124
132
|
0
|
125
133
|
end
|
@@ -232,6 +240,6 @@ class JobInvocationComposer
|
|
232
240
|
end
|
233
241
|
|
234
242
|
def validate_host_ids(ids)
|
235
|
-
Host.authorized(
|
243
|
+
Host.authorized(Targeting::RESOLVE_PERMISSION, Host).where(:id => ids).pluck(:id)
|
236
244
|
end
|
237
245
|
end
|
data/app/models/job_template.rb
CHANGED
@@ -13,7 +13,10 @@ class Setting::RemoteExecution < Setting
|
|
13
13
|
N_("Search for remote execution proxy outside of the proxies assigned to the host. " +
|
14
14
|
"If locations or organizations are enabled, the search will be limited to the host's " +
|
15
15
|
"organization or location."),
|
16
|
-
|
16
|
+
true),
|
17
|
+
self.set('remote_execution_ssh_user',
|
18
|
+
N_("Default user to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_user."),
|
19
|
+
'root'),
|
17
20
|
].each { |s| self.create! s.update(:category => "Setting::RemoteExecution") }
|
18
21
|
end
|
19
22
|
|
data/app/models/targeting.rb
CHANGED
@@ -3,6 +3,7 @@ class Targeting < ActiveRecord::Base
|
|
3
3
|
STATIC_TYPE = 'static_query'
|
4
4
|
DYNAMIC_TYPE = 'dynamic_query'
|
5
5
|
TYPES = { STATIC_TYPE => N_('Static Query'), DYNAMIC_TYPE => N_('Dynamic Query') }
|
6
|
+
RESOLVE_PERMISSION = :view_hosts
|
6
7
|
|
7
8
|
belongs_to :user
|
8
9
|
belongs_to :bookmark
|
@@ -26,7 +27,7 @@ class Targeting < ActiveRecord::Base
|
|
26
27
|
self.search_query = bookmark.query if dynamic? && bookmark.present?
|
27
28
|
self.touch(:resolved_at)
|
28
29
|
self.save!
|
29
|
-
self.hosts = User.as(user.login) { Host.authorized(
|
30
|
+
self.hosts = User.as(user.login) { Host.authorized(RESOLVE_PERMISSION, Host).search_for(search_query) }
|
30
31
|
end
|
31
32
|
|
32
33
|
def dynamic?
|
@@ -49,8 +50,9 @@ class Targeting < ActiveRecord::Base
|
|
49
50
|
private
|
50
51
|
|
51
52
|
def bookmark_or_query_is_present
|
52
|
-
if bookmark.
|
53
|
-
errors.add :
|
53
|
+
if bookmark.blank? && search_query.blank?
|
54
|
+
errors.add :bookmark_id, _('Must select a bookmark or enter a search query')
|
55
|
+
errors.add :search_query, _('Must select a bookmark or enter a search query')
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
@@ -17,6 +17,9 @@
|
|
17
17
|
<%= button_tag(:type => 'button', :class => 'btn btn-default btn-sm', :title => _("Refresh"), :id => 'refresh_execution_form') do %>
|
18
18
|
<%= icon_text('refresh') %>
|
19
19
|
<% end %>
|
20
|
+
<%= button_tag(:type => 'button', :class => 'btn btn-default btn-sm', :title => _("Preview"), :id => 'preview_hosts') do %>
|
21
|
+
<%= icon_text('eye-open') %>
|
22
|
+
<% end %>
|
20
23
|
</div>
|
21
24
|
</div>
|
22
25
|
|
@@ -80,6 +83,6 @@
|
|
80
83
|
<%= text_f f, :start_before, :label => _('Start before') %>
|
81
84
|
</fieldset>
|
82
85
|
</div>
|
83
|
-
|
86
|
+
<%= render :partial => 'preview_hosts_modal' %>
|
84
87
|
<%= submit_or_cancel f %>
|
85
88
|
<% end %>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<% if @dynamic -%>
|
2
|
+
<div class="alert alert-info alert-dismissable">
|
3
|
+
<p><%= _("The final host list may change because the selected query is dynamic. It will be rerun during execution.") %></p>
|
4
|
+
</div>
|
5
|
+
<% end -%>
|
6
|
+
|
7
|
+
<% if @hosts.any? -%>
|
8
|
+
<ul>
|
9
|
+
<% @hosts.each do |host| -%>
|
10
|
+
<li><%= link_to h(host.name), host_path(host), :target => '_blank' %></li>
|
11
|
+
<% end -%>
|
12
|
+
|
13
|
+
<% if @additional > 0 -%>
|
14
|
+
<li><%= link_to(_("...and %{count} more" % {:count => @additional}), hosts_path(:search => @query, :page => 2), :target => '_blank') %></li>
|
15
|
+
<% end -%>
|
16
|
+
</ul>
|
17
|
+
<% else -%>
|
18
|
+
<h3><%= _("No hosts found.") %></h3>
|
19
|
+
<% end -%>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<!-- modal window -->
|
2
|
+
<div class="modal fade" id="previewHostsModal" role="dialog" aria-hidden="true" data-url="<%= preview_hosts_job_invocations_path %>">
|
3
|
+
<div class="modal-dialog">
|
4
|
+
<div class="modal-content">
|
5
|
+
<div class="modal-header">
|
6
|
+
<button type="button" class="close" onclick="close_preview_hosts_modal(); return false;"><span aria-hidden="true">×</span><span class="sr-only"><%= _('Close') %></span></button>
|
7
|
+
<h4 class="modal-title">Preview Hosts</h4>
|
8
|
+
</div>
|
9
|
+
<div class="modal-body" style="overflow-y: auto; max-height: 500px;">
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
</div>
|
13
|
+
</div>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<%#
|
2
|
+
kind: snippet
|
3
|
+
name: remote_execution_ssh_keys
|
4
|
+
%>
|
5
|
+
|
6
|
+
<% if @host.params['remote_execution_ssh_keys'].present? %>
|
7
|
+
<% ssh_user = @host.params['remote_execution_ssh_user'] || 'root' %>
|
8
|
+
<% ssh_path = "~#{ssh_user}/.ssh" %>
|
9
|
+
|
10
|
+
mkdir -p <%= ssh_path %>
|
11
|
+
|
12
|
+
cat << EOF >> <%= ssh_path %>/authorized_keys
|
13
|
+
<%= @host.params['remote_execution_ssh_keys'].join("\n") %>
|
14
|
+
EOF
|
15
|
+
|
16
|
+
chmod 700 <%= ssh_path %>
|
17
|
+
chmod 600 <%= ssh_path %>/authorized_keys
|
18
|
+
<% end %>
|
data/config/routes.rb
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
ProvisioningTemplate.without_auditing do
|
2
|
+
templates = [{:name => "remote_execution_ssh_keys", :source => "snippets/_remote_execution_ssh_keys.erb", :snippet => true}]
|
3
|
+
|
4
|
+
defaults = {:vendor => "Remote Execution", :default => true, :locked => true}
|
5
|
+
|
6
|
+
templates.each do |template|
|
7
|
+
next if ProvisioningTemplate.find_by_name(template[:name])
|
8
|
+
|
9
|
+
template.merge!(defaults)
|
10
|
+
|
11
|
+
t= ProvisioningTemplate.create({
|
12
|
+
:snippet => false,
|
13
|
+
:template => File.read(File.join("#{ForemanRemoteExecution::Engine.root}/app/views/unattended", template.delete(:source)))
|
14
|
+
}.merge(template))
|
15
|
+
|
16
|
+
t.organizations << (Organization.all - t.organizations)
|
17
|
+
t.locations << (Location.all - t.locations)
|
18
|
+
|
19
|
+
raise "Unable to create template #{t.name}: #{format_errors t}" if t.nil? || t.errors.any?
|
20
|
+
end
|
21
|
+
end
|
@@ -27,7 +27,7 @@ module ForemanRemoteExecution
|
|
27
27
|
|
28
28
|
initializer 'foreman_remote_execution.register_plugin', after: :finisher_hook do |_app|
|
29
29
|
Foreman::Plugin.register :foreman_remote_execution do
|
30
|
-
requires_foreman '>= 1.
|
30
|
+
requires_foreman '>= 1.10'
|
31
31
|
|
32
32
|
apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
|
33
33
|
|
@@ -42,7 +42,7 @@ module ForemanRemoteExecution
|
|
42
42
|
permission :destroy_job_templates, { :job_templates => [:destroy],
|
43
43
|
:'api/v2/job_templates' => [:destroy] }, :resource_type => 'JobTemplate'
|
44
44
|
permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
|
45
|
-
permission :create_job_invocations, { :job_invocations => [:new, :create, :refresh, :rerun],
|
45
|
+
permission :create_job_invocations, { :job_invocations => [:new, :create, :refresh, :rerun, :preview_hosts],
|
46
46
|
'api/v2/job_invocations' => [:create] }, :resource_type => 'JobInvocation'
|
47
47
|
permission :view_job_invocations, { :job_invocations => [:index, :show, :auto_complete_search], :template_invocations => [:show],
|
48
48
|
'api/v2/job_invocations' => [:index, :show] }, :resource_type => 'JobInvocation'
|
@@ -64,6 +64,7 @@ module ForemanRemoteExecution
|
|
64
64
|
parent: :monitor_menu,
|
65
65
|
after: :audits
|
66
66
|
|
67
|
+
register_custom_status HostStatus::ExecutionStatus
|
67
68
|
# add dashboard widget
|
68
69
|
# widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
|
69
70
|
end
|
@@ -103,10 +104,9 @@ module ForemanRemoteExecution
|
|
103
104
|
(Taxonomy.descendants + [Taxonomy]).each { |klass| klass.send(:include, ForemanRemoteExecution::TaxonomyExtensions) }
|
104
105
|
|
105
106
|
User.send(:include, ForemanRemoteExecution::UserExtensions)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
end
|
107
|
+
|
108
|
+
Host::Managed.send(:include, ForemanRemoteExecution::HostExtensions)
|
109
|
+
Host::Managed.send(:include, ForemanTasks::Concerns::HostActionSubject)
|
110
110
|
|
111
111
|
(Nic::Base.descendants + [Nic::Base]).each do |klass|
|
112
112
|
klass.send(:include, ForemanRemoteExecution::NicExtensions)
|
@@ -115,6 +115,7 @@ module ForemanRemoteExecution
|
|
115
115
|
Bookmark.send(:include, ForemanRemoteExecution::BookmarkExtensions)
|
116
116
|
HostsHelper.send(:include, ForemanRemoteExecution::HostsHelperExtensions)
|
117
117
|
|
118
|
+
SmartProxy.send(:include, ForemanRemoteExecution::SmartProxyExtensions)
|
118
119
|
Subnet.send(:include, ForemanRemoteExecution::SubnetExtensions)
|
119
120
|
|
120
121
|
# We need to explicitly force to load the Task model due to Rails loader
|
@@ -29,7 +29,7 @@ module Api
|
|
29
29
|
|
30
30
|
invocation = ActiveSupport::JSON.decode(@response.body)
|
31
31
|
assert_equal attrs[:job_name], invocation['job_name']
|
32
|
-
assert_response
|
32
|
+
assert_response :success
|
33
33
|
end
|
34
34
|
|
35
35
|
test "should create valid with template_id" do
|
@@ -38,7 +38,7 @@ module Api
|
|
38
38
|
|
39
39
|
invocation = ActiveSupport::JSON.decode(@response.body)
|
40
40
|
assert_equal attrs[:job_name], invocation['job_name']
|
41
|
-
assert_response
|
41
|
+
assert_response :success
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
@@ -9,6 +9,35 @@ describe ForemanRemoteExecution::HostExtensions do
|
|
9
9
|
|
10
10
|
after { User.current = nil }
|
11
11
|
|
12
|
+
context 'ssh user' do
|
13
|
+
let(:host) { FactoryGirl.build(:host, :with_execution) }
|
14
|
+
let(:sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQ foo@example.com' }
|
15
|
+
|
16
|
+
before do
|
17
|
+
SmartProxy.any_instance.stubs(:pubkey).returns(sshkey)
|
18
|
+
Setting[:remote_execution_ssh_user] = 'root'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'has ssh user in the parameters' do
|
22
|
+
host.params['remote_execution_ssh_user'].must_equal Setting[:remote_execution_ssh_user]
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'can override ssh user' do
|
26
|
+
host.host_parameters << FactoryGirl.build(:host_parameter, :name => 'remote_execution_ssh_user', :value => 'amy')
|
27
|
+
host.params['remote_execution_ssh_user'].must_equal 'amy'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'ssh keys' do
|
32
|
+
let(:host) { FactoryGirl.build(:host, :with_execution) }
|
33
|
+
let(:sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQ foo@example.com' }
|
34
|
+
|
35
|
+
it 'has ssh keys in the parameters' do
|
36
|
+
SmartProxy.any_instance.stubs(:pubkey).returns(sshkey)
|
37
|
+
host.remote_execution_ssh_keys.must_include sshkey
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
12
41
|
context 'host has multiple nics' do
|
13
42
|
let(:host) { FactoryGirl.build(:host, :with_execution) }
|
14
43
|
|
@@ -317,14 +317,14 @@ describe JobInvocationComposer do
|
|
317
317
|
end
|
318
318
|
|
319
319
|
describe '#targeted_hosts_count' do
|
320
|
+
let(:host) { FactoryGirl.create(:host) }
|
321
|
+
|
320
322
|
it 'obeys authorization' do
|
321
|
-
composer
|
323
|
+
composer.stubs(:displayed_search_query => "name = #{host.name}")
|
322
324
|
Host.expects(:authorized).with(:view_hosts, Host).returns(Host.scoped)
|
323
325
|
composer.targeted_hosts_count
|
324
326
|
end
|
325
327
|
|
326
|
-
let(:host) { FactoryGirl.create(:host) }
|
327
|
-
|
328
328
|
it 'searches hosts based on displayed_search_query' do
|
329
329
|
composer.stubs(:displayed_search_query => "name = #{host.name}")
|
330
330
|
composer.targeted_hosts_count.must_equal 1
|
@@ -334,6 +334,11 @@ describe JobInvocationComposer do
|
|
334
334
|
composer.stubs(:displayed_search_query => "name = ")
|
335
335
|
composer.targeted_hosts_count.must_equal 0
|
336
336
|
end
|
337
|
+
|
338
|
+
it 'returns 0 when no query is present' do
|
339
|
+
composer.stubs(:displayed_search_query => '')
|
340
|
+
composer.targeted_hosts_count.must_equal 0
|
341
|
+
end
|
337
342
|
end
|
338
343
|
|
339
344
|
describe '#template_invocation_input_value_for(input)' do
|
@@ -1,6 +1,19 @@
|
|
1
1
|
require 'test_plugin_helper'
|
2
2
|
|
3
3
|
describe JobTemplate do
|
4
|
+
context 'when creating a template' do
|
5
|
+
let(:job_template) { FactoryGirl.build(:job_template, :job_name => '') }
|
6
|
+
|
7
|
+
it 'needs a job_name' do
|
8
|
+
refute job_template.valid?
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'does not need a job_name if it is a snippet' do
|
12
|
+
job_template.snippet = true
|
13
|
+
assert job_template.valid?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
4
17
|
context 'cloning' do
|
5
18
|
let(:job_template) { FactoryGirl.build(:job_template, :with_input) }
|
6
19
|
|
data/test/unit/targeting_test.rb
CHANGED
@@ -38,7 +38,7 @@ describe Targeting do
|
|
38
38
|
context 'cannot create without search term or bookmark' do
|
39
39
|
before do
|
40
40
|
targeting.targeting_type = Targeting::DYNAMIC_TYPE
|
41
|
-
targeting.search_query =
|
41
|
+
targeting.search_query = ''
|
42
42
|
targeting.bookmark = nil
|
43
43
|
end
|
44
44
|
it { refute_valid targeting }
|
metadata
CHANGED
@@ -1,83 +1,83 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10
|
11
|
+
date: 2015-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rails
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: 3.2.8
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 3.2.8
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: foreman-tasks
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: 0.7.6
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.7.6
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rubocop
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rdoc
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
description: A plugin bringing remote execution to the Foreman, completing the config
|
@@ -127,10 +127,10 @@ extra_rdoc_files:
|
|
127
127
|
- README.md
|
128
128
|
- LICENSE
|
129
129
|
files:
|
130
|
-
- .gitignore
|
131
|
-
- .rubocop.yml
|
132
|
-
- .rubocop_todo.yml
|
133
|
-
- .tx/config
|
130
|
+
- ".gitignore"
|
131
|
+
- ".rubocop.yml"
|
132
|
+
- ".rubocop_todo.yml"
|
133
|
+
- ".tx/config"
|
134
134
|
- Gemfile
|
135
135
|
- LICENSE
|
136
136
|
- README.md
|
@@ -151,17 +151,20 @@ files:
|
|
151
151
|
- app/lib/actions/remote_execution/run_host_job.rb
|
152
152
|
- app/lib/actions/remote_execution/run_hosts_job.rb
|
153
153
|
- app/lib/actions/remote_execution/run_proxy_command.rb
|
154
|
+
- app/lib/proxy_api/remote_execution_ssh.rb
|
154
155
|
- app/mailers/.gitkeep
|
155
156
|
- app/models/concerns/foreman_remote_execution/bookmark_extensions.rb
|
156
157
|
- app/models/concerns/foreman_remote_execution/errors_flattener.rb
|
157
158
|
- app/models/concerns/foreman_remote_execution/foreman_tasks_task_extensions.rb
|
158
159
|
- app/models/concerns/foreman_remote_execution/host_extensions.rb
|
159
160
|
- app/models/concerns/foreman_remote_execution/nic_extensions.rb
|
161
|
+
- app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb
|
160
162
|
- app/models/concerns/foreman_remote_execution/subnet_extensions.rb
|
161
163
|
- app/models/concerns/foreman_remote_execution/taxonomy_extensions.rb
|
162
164
|
- app/models/concerns/foreman_remote_execution/template_extensions.rb
|
163
165
|
- app/models/concerns/foreman_remote_execution/template_relations.rb
|
164
166
|
- app/models/concerns/foreman_remote_execution/user_extensions.rb
|
167
|
+
- app/models/host_status/execution_status.rb
|
165
168
|
- app/models/input_template_renderer.rb
|
166
169
|
- app/models/job_invocation.rb
|
167
170
|
- app/models/job_invocation_api_composer.rb
|
@@ -197,6 +200,8 @@ files:
|
|
197
200
|
- app/views/job_invocations/_host_actions_td.html.erb
|
198
201
|
- app/views/job_invocations/_host_provider_td.html.erb
|
199
202
|
- app/views/job_invocations/_host_status_td.html.erb
|
203
|
+
- app/views/job_invocations/_preview_hosts_list.html.erb
|
204
|
+
- app/views/job_invocations/_preview_hosts_modal.html.erb
|
200
205
|
- app/views/job_invocations/_tab_hosts.html.erb
|
201
206
|
- app/views/job_invocations/_tab_overview.html.erb
|
202
207
|
- app/views/job_invocations/index.html.erb
|
@@ -219,6 +224,7 @@ files:
|
|
219
224
|
- app/views/templates/puppet_run_once.erb
|
220
225
|
- app/views/templates/run_command.erb
|
221
226
|
- app/views/templates/service_action.erb
|
227
|
+
- app/views/unattended/snippets/_remote_execution_ssh_keys.erb
|
222
228
|
- config/routes.rb
|
223
229
|
- db/migrate/20150612121541_add_job_template_to_template.rb
|
224
230
|
- db/migrate/20150616080015_create_template_input.rb
|
@@ -231,8 +237,10 @@ files:
|
|
231
237
|
- db/migrate/20150827144500_change_targeting_search_query_type.rb
|
232
238
|
- db/migrate/20150827152730_add_options_to_template_input.rb
|
233
239
|
- db/migrate/20150903192731_add_execution_to_interface.rb
|
240
|
+
- db/migrate/20151013135415_add_pub_key_to_smart_proxy.rb
|
234
241
|
- db/seeds.d/60-ssh_proxy_feature.rb
|
235
242
|
- db/seeds.d/70-job_templates.rb
|
243
|
+
- db/seeds.d/80-provision_templates.rb
|
236
244
|
- doc/.bin/.gitkeep
|
237
245
|
- doc/.gitignore
|
238
246
|
- doc/Gemfile
|
@@ -306,12 +314,12 @@ require_paths:
|
|
306
314
|
- lib
|
307
315
|
required_ruby_version: !ruby/object:Gem::Requirement
|
308
316
|
requirements:
|
309
|
-
- -
|
317
|
+
- - ">="
|
310
318
|
- !ruby/object:Gem::Version
|
311
319
|
version: '0'
|
312
320
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
313
321
|
requirements:
|
314
|
-
- -
|
322
|
+
- - ">="
|
315
323
|
- !ruby/object:Gem::Version
|
316
324
|
version: '0'
|
317
325
|
requirements: []
|
@@ -340,4 +348,3 @@ test_files:
|
|
340
348
|
- test/unit/targeting_test.rb
|
341
349
|
- test/unit/template_input_test.rb
|
342
350
|
- test/unit/template_invocation_input_value_test.rb
|
343
|
-
has_rdoc:
|