foreman_remote_execution 0.0.10 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: