foreman_remote_execution 0.0.1

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