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.
Files changed (32) hide show
  1. checksums.yaml +5 -13
  2. data/README.md +14 -21
  3. data/app/assets/javascripts/template_invocation.js +29 -0
  4. data/app/controllers/job_invocations_controller.rb +13 -0
  5. data/app/controllers/job_templates_controller.rb +1 -1
  6. data/app/helpers/remote_execution_helper.rb +11 -4
  7. data/app/lib/actions/remote_execution/run_host_job.rb +19 -3
  8. data/app/lib/proxy_api/remote_execution_ssh.rb +14 -0
  9. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +27 -1
  10. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +26 -0
  11. data/app/models/host_status/execution_status.rb +49 -0
  12. data/app/models/job_invocation_composer.rb +10 -2
  13. data/app/models/job_template.rb +1 -0
  14. data/app/models/setting/remote_execution.rb +4 -1
  15. data/app/models/targeting.rb +5 -3
  16. data/app/views/job_invocations/_form.html.erb +4 -1
  17. data/app/views/job_invocations/_preview_hosts_list.html.erb +19 -0
  18. data/app/views/job_invocations/_preview_hosts_modal.html.erb +13 -0
  19. data/app/views/job_invocations/new.html.erb +1 -5
  20. data/app/views/job_invocations/show.js.erb +1 -0
  21. data/app/views/unattended/snippets/_remote_execution_ssh_keys.erb +18 -0
  22. data/config/routes.rb +1 -0
  23. data/db/migrate/20151013135415_add_pub_key_to_smart_proxy.rb +5 -0
  24. data/db/seeds.d/80-provision_templates.rb +21 -0
  25. data/lib/foreman_remote_execution/engine.rb +7 -6
  26. data/lib/foreman_remote_execution/version.rb +1 -1
  27. data/test/functional/api/v2/job_invocations_controller_test.rb +2 -2
  28. data/test/unit/concerns/host_extensions_test.rb +29 -0
  29. data/test/unit/job_invocation_composer_test.rb +8 -3
  30. data/test/unit/job_template_test.rb +13 -0
  31. data/test/unit/targeting_test.rb +1 -1
  32. metadata +26 -19
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NGRlODQ0Y2VkM2NhNDdjMmQwOGI1Nzg4NzYxODg4ZjBlNTkxY2NiNQ==
5
- data.tar.gz: !binary |-
6
- ZGY2YjZkZGY4MzliZmMxMzg5Y2U2Yjg0NGRiNzQ5ZjZkZjA4YmIzYg==
2
+ SHA1:
3
+ metadata.gz: 9a83d62de427a930126dbdac52051e2a6ca6e0cc
4
+ data.tar.gz: a304bc71914618eb42fa1648b4f6ce3f2f31fceb
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MzE5ZjU3MjUyZTRhNTY5OTIwNzU1OWRjZTgwMzFmMmVlMzgxOTUzNzJkZWM2
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
- ## Installation
11
+ * Website: [theforeman.org](http://theforeman.org)
12
+ * Support: [Foreman support](http://theforeman.org/support.html)
12
13
 
13
- See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
14
- for how to install Foreman plugins
14
+ ## Features
15
15
 
16
- ## Usage
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
- *Usage here*
23
+ ## Installation and usage
19
24
 
20
- ## Generating the docs
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
- * [the project page](http://theforeman.github.io/foreman_remote_execution/)
37
- * [the issue tracker](http://projects.theforeman.org/projects/foreman_remote_execution)
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| %w(warning error).include? sub_task.result }
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 => :view_foreman_tasks))]
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(:permission => :create_job_invocations)
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(:permission => :create_job_invocations)
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
- plan_action(RunProxyCommand, proxy, hostname, script, { :connection_options => connection_options })
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
- Host.authorized(:view_hosts, Host).search_for(displayed_search_query).count
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(:view_hosts, Host).where(:id => ids).pluck(:id)
243
+ Host.authorized(Targeting::RESOLVE_PERMISSION, Host).where(:id => ids).pluck(:id)
236
244
  end
237
245
  end
@@ -28,6 +28,7 @@ class JobTemplate < ::Template
28
28
  end
29
29
  }
30
30
 
31
+ validates :job_name, :presence => true, :unless => ->(job_template) { job_template.snippet }
31
32
  validates :provider_type, :presence => true
32
33
  validate :provider_type_whitelist
33
34
 
@@ -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
- false),
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
 
@@ -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('edit_hosts', Host).search_for(search_query) }
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.nil? && search_query.nil?
53
- errors.add :base, _('Bookmark or search query can\'t be nil')
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">&times;</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>
@@ -1,8 +1,4 @@
1
1
  <% javascript 'template_invocation' %>
2
2
  <% stylesheet 'template_invocation' %>
3
-
4
- <div class="row form-group">
5
- <h1 class="col-md-8"><%= _('New Job Invocation') %></h1>
6
- </div>
7
-
3
+ <% title _('Job invocation') %>
8
4
  <%= render :partial => 'form' %>
@@ -1,3 +1,4 @@
1
+ $('div.btn-group').html('<%= button_group(job_invocation_task_buttons(@job_invocation.last_task)).html_safe %>');
1
2
  $('div#status_chart').html('<%=j job_invocation_chart(@job_invocation) %>');
2
3
  $('div#status').flot_pie();
3
4
 
@@ -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 %>
@@ -16,6 +16,7 @@ Rails.application.routes.draw do
16
16
  resources :job_invocations, :only => [:new, :create, :show, :index] do
17
17
  collection do
18
18
  post 'refresh'
19
+ get 'preview_hosts'
19
20
  get 'auto_complete_search'
20
21
  end
21
22
  member do
@@ -0,0 +1,5 @@
1
+ class AddPubKeyToSmartProxy < ActiveRecord::Migration
2
+ def change
3
+ add_column :smart_proxies, :pubkey, :text
4
+ end
5
+ end
@@ -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.9'
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
- (Host::Base.descendants + [Host::Base]).each do |klass|
107
- klass.send(:include, ForemanRemoteExecution::HostExtensions)
108
- klass.send(:include, ForemanTasks::Concerns::HostActionSubject)
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
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '0.0.10'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -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 200
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 200
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
 
@@ -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 = nil
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.10
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-09 00:00:00.000000000 Z
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: