foreman_remote_execution 1.4.5 → 1.4.6

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 (42) hide show
  1. checksums.yaml +5 -5
  2. data/.hound.yml +13 -0
  3. data/.rubocop.yml +9 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +49 -6
  5. data/app/controllers/job_invocations_controller.rb +29 -1
  6. data/app/helpers/remote_execution_helper.rb +5 -5
  7. data/app/lib/actions/remote_execution/run_host_job.rb +32 -20
  8. data/app/lib/actions/remote_execution/run_hosts_job.rb +13 -8
  9. data/app/models/foreign_input_set.rb +1 -1
  10. data/app/models/job_invocation.rb +23 -1
  11. data/app/models/job_invocation_composer.rb +25 -3
  12. data/app/models/job_template.rb +2 -2
  13. data/app/models/remote_execution_feature.rb +22 -10
  14. data/app/models/setting/remote_execution.rb +34 -10
  15. data/app/models/ssh_execution_provider.rb +8 -0
  16. data/app/models/template_input.rb +1 -1
  17. data/app/views/api/v2/job_templates/base.json.rabl +1 -1
  18. data/app/views/job_invocations/_form.html.erb +11 -5
  19. data/app/views/job_invocations/_tab_hosts.html.erb +1 -1
  20. data/app/views/job_invocations/index.html.erb +1 -1
  21. data/app/views/job_templates/index.html.erb +1 -1
  22. data/app/views/remote_execution_features/index.html.erb +1 -1
  23. data/app/views/template_inputs/_invocation_form.html.erb +2 -2
  24. data/app/views/template_invocations/show.html.erb +1 -1
  25. data/config/routes.rb +5 -0
  26. data/db/migrate/20160113162007_expand_all_template_invocations.rb +1 -1
  27. data/db/migrate/20171129103615_add_secrets_to_job_invocations.rb +6 -0
  28. data/db/migrate/20180202072115_add_notification_builder_to_remote_execution_feature.rb +5 -0
  29. data/db/migrate/20180202123215_add_feature_id_to_job_invocation.rb +6 -0
  30. data/db/migrate/20180226095631_change_task_id_to_uuid.rb +31 -0
  31. data/foreman_remote_execution.gemspec +1 -1
  32. data/lib/foreman_remote_execution/engine.rb +3 -1
  33. data/lib/foreman_remote_execution/version.rb +1 -1
  34. data/test/factories/foreman_remote_execution_factories.rb +6 -0
  35. data/test/functional/api/v2/job_invocations_controller_test.rb +139 -32
  36. data/test/functional/job_invocations_controller_test.rb +49 -0
  37. data/test/unit/actions/run_hosts_job_test.rb +10 -6
  38. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +1 -1
  39. data/test/unit/job_invocation_composer_test.rb +43 -1
  40. metadata +14 -10
  41. data/app/models/concerns/foreman_remote_execution/exportable.rb +0 -71
  42. data/test/unit/concerns/exportable_test.rb +0 -88
@@ -1,10 +1,10 @@
1
1
  class JobTemplate < ::Template
2
- include ForemanRemoteExecution::Exportable
2
+ include ::Exportable
3
3
 
4
4
  class NonUniqueInputsError < Foreman::Exception
5
5
  end
6
6
 
7
- attr_exportable :name, :job_category, :description_format, :snippet, :template_inputs,
7
+ attr_exportable :job_category, :description_format, :template_inputs,
8
8
  :foreign_input_sets, :provider_type, :kind => ->(template) { template.class.name.underscore }
9
9
 
10
10
  include Authorizable
@@ -1,9 +1,11 @@
1
1
  class RemoteExecutionFeature < ApplicationRecord
2
- VALID_OPTIONS = [:provided_inputs, :description, :host_action_button].freeze
2
+ VALID_OPTIONS = [:provided_inputs, :description, :host_action_button, :notification_builder].freeze
3
3
  validates :label, :name, :presence => true, :uniqueness => true
4
4
 
5
5
  belongs_to :job_template
6
6
 
7
+ audited :only => :job_template_id
8
+
7
9
  extend FriendlyId
8
10
  friendly_id :label
9
11
 
@@ -27,19 +29,29 @@ class RemoteExecutionFeature < ApplicationRecord
27
29
  options[:host_action_button] = false unless options.key?(:host_action_button)
28
30
 
29
31
  feature = self.find_by(label: label)
32
+ builder = options[:notification_builder] ? options[:notification_builder].to_s : nil
30
33
 
31
- attributes = { :name => name, :provided_input_names => options[:provided_inputs], :description => options[:description], :host_action_button => options[:host_action_button] }
34
+ attributes = { :name => name,
35
+ :provided_input_names => options[:provided_inputs],
36
+ :description => options[:description],
37
+ :host_action_button => options[:host_action_button],
38
+ :notification_builder => builder }
32
39
  # in case DB does not have the attribute created yet but plugin initializer registers the feature, we need to skip this attribute
33
- unless self.attribute_names.include?('host_action_button')
34
- attributes.delete(:host_action_button)
40
+ attrs = [ :host_action_button, :notification_builder ]
41
+ attrs.each do |attr|
42
+ unless self.attribute_names.include?(attr.to_s)
43
+ attributes.delete(attr)
44
+ end
35
45
  end
36
46
 
37
- if feature.nil?
38
- feature = self.create!({ :label => label }.merge(attributes))
39
- else
40
- feature.attributes = attributes
41
- feature.save if feature.changed?
47
+ self.without_auditing do
48
+ if feature.nil?
49
+ feature = self.create!({ :label => label }.merge(attributes))
50
+ else
51
+ feature.attributes = attributes
52
+ feature.save if feature.changed?
53
+ end
54
+ return feature
42
55
  end
43
- return feature
44
56
  end
45
57
  end
@@ -1,5 +1,8 @@
1
1
  class Setting::RemoteExecution < Setting
2
2
 
3
+ ::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase}
4
+
5
+ # rubocop:disable Metrics/MethodLength
3
6
  def self.load_defaults
4
7
  # Check the table exists
5
8
  return unless super
@@ -9,42 +12,63 @@ class Setting::RemoteExecution < Setting
9
12
  [
10
13
  self.set('remote_execution_fallback_proxy',
11
14
  N_('Search the host for any proxy with Remote Execution, useful when the host has no subnet or the subnet does not have an execution proxy'),
12
- false),
15
+ false,
16
+ N_('Fallback to Any Proxy')),
13
17
  self.set('remote_execution_global_proxy',
14
18
  N_('Search for remote execution proxy outside of the proxies assigned to the host. ' +
15
19
  "If locations or organizations are enabled, the search will be limited to the host's " +
16
20
  'organization or location.'),
17
- true),
21
+ true,
22
+ N_('Enable Global Proxy')),
18
23
  self.set('remote_execution_without_proxy',
19
24
  N_('When enabled, the remote execution will try to run the commands directly, when no
20
25
  proxy with remote execution feature is configured for the host.'),
21
- false),
26
+ false,
27
+ N_('Fallback Without Proxy')),
22
28
  self.set('remote_execution_ssh_user',
23
29
  N_('Default user to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_user.'),
24
- 'root'),
30
+ 'root',
31
+ N_('SSH User')),
25
32
  self.set('remote_execution_effective_user',
26
33
  N_('Default user to use for executing the script. If the user differs from the SSH user, su or sudo is used to switch the user.'),
27
- 'root'),
34
+ 'root',
35
+ N_('Effective User')),
28
36
  self.set('remote_execution_effective_user_method',
29
37
  N_('What command should be used to switch to the effective user. One of %s') % SSHExecutionProvider::EFFECTIVE_USER_METHODS.inspect,
30
38
  'sudo',
31
- 'remote_execution_effective_user_method',
39
+ N_('Effective User Method'),
32
40
  nil,
33
41
  { :collection => proc { Hash[SSHExecutionProvider::EFFECTIVE_USER_METHODS.map { |method| [method, method] }] } }),
34
42
  self.set('remote_execution_sync_templates',
35
43
  N_('Whether we should sync templates from disk when running db:seed.'),
36
- true),
44
+ true,
45
+ N_('Sync Job Templates')),
37
46
  self.set('remote_execution_ssh_port',
38
47
  N_('Port to use for SSH communication. Default port 22. You may override per host by setting a parameter called remote_execution_ssh_port.'),
39
- '22'),
48
+ '22',
49
+ N_('SSH Port')),
40
50
  self.set('remote_execution_connect_by_ip',
41
51
  N_('Should the ip addresses on host interfaces be preferred over the fqdn? '\
42
52
  'It is useful, when DNS not resolving the fqdns properly. You may override this per host by setting a parameter called remote_execution_connect_by_ip.'),
43
- false)
53
+ false,
54
+ N_('Connect by IP')),
55
+ self.set('remote_execution_ssh_password',
56
+ N_('Default password to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_password'),
57
+ nil,
58
+ N_('Default SSH password'),
59
+ nil,
60
+ { :encrypted => true }),
61
+ self.set('remote_execution_ssh_key_passphrase',
62
+ N_('Default key passphrase to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_key_passphrase'),
63
+ nil,
64
+ N_('Default SSH key passphrase'),
65
+ nil,
66
+ { :encrypted => true })
44
67
  ].each { |s| self.create! s.update(:category => 'Setting::RemoteExecution') }
45
68
  end
46
69
 
47
70
  true
48
71
  end
49
-
72
+ # rubocop:enable AbcSize
73
+ # rubocop:enable Metrics/MethodLength
50
74
  end
@@ -15,6 +15,14 @@ class SSHExecutionProvider < RemoteExecutionProvider
15
15
  true
16
16
  end
17
17
 
18
+ def ssh_password(host)
19
+ host_setting(host, :remote_execution_ssh_password)
20
+ end
21
+
22
+ def ssh_key_passphrase(host)
23
+ host_setting(host, :remote_execution_ssh_key_passphrase)
24
+ end
25
+
18
26
  private
19
27
 
20
28
  def ssh_user(host)
@@ -1,5 +1,5 @@
1
1
  class TemplateInput < ApplicationRecord
2
- include ForemanRemoteExecution::Exportable
2
+ include ::Exportable
3
3
 
4
4
  class ValueNotReady < ::Foreman::Exception
5
5
  end
@@ -1,3 +1,3 @@
1
1
  object @job_template
2
2
 
3
- attributes :id, :name, :job_category, :provider_type, :snippet, :description_format
3
+ attributes :id, :name, :job_category, :provider_type, :snippet, :description_format, :created_at, :updated_at
@@ -7,6 +7,7 @@
7
7
  <%= form_for @composer.job_invocation, :html => {'data-refresh-url' => refresh_job_invocations_path, :id => 'job_invocation_form'} do |f| %>
8
8
 
9
9
  <%= selectable_f f, :job_category, @composer.available_job_categories, {}, :label => _('Job category') %>
10
+ <%= f.hidden_field(:remote_execution_feature_id, :value => @composer.remote_execution_feature_id) %>
10
11
 
11
12
  <% selected_templates_per_provider = {} %>
12
13
  <% @composer.displayed_provider_types.each do |provider_type| %>
@@ -72,13 +73,13 @@
72
73
 
73
74
  <div class="advanced hidden">
74
75
  <% if job_template.effective_user.overridable? %>
75
- <%= text_f job_template_fields, :effective_user, :value => @composer.template_invocation(job_template).try(:effective_user), :label => _('Effective user'), :help_inline => N_("A user to be used for executing the script. If it differs from the SSH user, su or sudo is used to switch the accounts.") %>
76
+ <%= text_f job_template_fields, :effective_user, :value => @composer.template_invocation(job_template).try(:effective_user), :label => _('Effective user'), :label_help => N_("A user to be used for executing the script. If it differs from the SSH user, su or sudo is used to switch the accounts.") %>
76
77
  <% end %>
77
78
  <%= render :partial => 'description_fields', :locals => { :f => f, :job_template => job_template, :disabled => job_template != selected_templates_per_provider[provider_type] } %>
78
79
  </div>
79
80
 
80
81
  <div class="advanced hidden">
81
- <%= number_f job_template_fields, :execution_timeout_interval, :value => f.object.execution_timeout_interval || job_template.execution_timeout_interval, :label => _('Timeout to kill'), :help_inline => N_('Time in seconds from the start on the remote host after which the job should be killed.') %>
82
+ <%= number_f job_template_fields, :execution_timeout_interval, :value => f.object.execution_timeout_interval || job_template.execution_timeout_interval, :label => _('Timeout to kill'), :label_help => N_('Time in seconds from the start on the remote host after which the job should be killed.') %>
82
83
  </div>
83
84
  <% end %>
84
85
  </fieldset>
@@ -90,12 +91,17 @@
90
91
  <% end %>
91
92
 
92
93
  <div class="advanced hidden">
93
- <%= number_f f, :concurrency_level, :label => _('Concurrency level'), :placeholder => 'N', :min => 1, :help_inline => N_("Run at most N tasks at a time") %>
94
- <%= number_f f, :time_span, :label => _('Time span'), :placeholder => 'N', :min => 1, :help_inline => N_("Distribute execution over N seconds") %>
94
+ <%= password_f f, :password, :placeholder => '*****', :label => _('Password'), :label_help => N_('Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
95
+ <%= password_f f, :key_passphrase, :placeholder => '*****', :label => _('Private key passphrase'), :label_help => N_('Key passhprase is only applicable for SSH provider. Other providers ignore this field. <br> Passphrase is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
96
+ </div>
97
+
98
+ <div class="advanced hidden">
99
+ <%= number_f f, :concurrency_level, :label => _('Concurrency level'), :placeholder => 'N', :min => 1, :label_help => N_("Run at most N tasks at a time") %>
100
+ <%= number_f f, :time_span, :label => _('Time span'), :placeholder => 'N', :min => 1, :label_help => N_("Distribute execution over N seconds") %>
95
101
  </div>
96
102
 
97
103
  <div class="form-group advanced hidden">
98
- <%= add_label({ :label => _('Type of query'), :label_help => _("Type has impact on when is the query evaulated to hosts.<br><ul><li><b>Static</b> - evaluates just after you submit this form</li><li><b>Dynamic</b> - evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it</li></ul>") }, f, :targetting_type) %>
104
+ <%= add_label({ :label => _('Type of query'), :label_help => _("Type has impact on when is the query evaluated to hosts.<br><ul><li><b>Static</b> - evaluates just after you submit this form</li><li><b>Dynamic</b> - evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it</li></ul>") }, f, :targetting_type) %>
99
105
 
100
106
  <div class="col-md-4">
101
107
  <%= radio_button_f targeting_fields, :targeting_type, :value => Targeting::STATIC_TYPE, :text => _(Targeting::TYPES[Targeting::STATIC_TYPE]) %>
@@ -15,7 +15,7 @@
15
15
  <% end %>
16
16
  <br>
17
17
 
18
- <table class="table table-bordered table-striped table-condensed">
18
+ <table class="<%= table_css_classes('table-condensed') %>">
19
19
  <thead>
20
20
  <tr>
21
21
  <th><%= sort :name, :as => _('Host') %></th>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <% title_actions(job_invocations_buttons) %>
4
4
 
5
- <table class="table table-bordered table-striped table-condensed table-fixed">
5
+ <table class="<%= table_css_classes('table-condensed table-fixed') %>">
6
6
  <thead>
7
7
  <tr>
8
8
  <th><%= sort :description, :as => _('Description') %></th>
@@ -7,7 +7,7 @@
7
7
  link_to_function(_('Import'), 'show_import_job_template_modal();', :class => 'btn btn-default'),
8
8
  new_link(_("New Job Template"))) %>
9
9
 
10
- <table class="table table-bordered table-striped table-two-pane table-fixed">
10
+ <table class="<%= table_css_classes('table-two-pane table-fixed') %>">
11
11
  <thead>
12
12
  <tr>
13
13
  <th class="col-md-6"><%= sort :name, :as => s_("JobTemplate|Name") %></th>
@@ -1,6 +1,6 @@
1
1
  <% title _('Remote Execution Features') %>
2
2
 
3
- <table class="table table-bordered table-striped table-condensed">
3
+ <table class="<%= table_css_classes('table-condensed') %>">
4
4
  <thead>
5
5
  <tr>
6
6
  <th><%= sort :label, :as => _('Label') %></th>
@@ -1,7 +1,7 @@
1
1
  <%= input_values_fields.fields_for input.id.to_s, composer.template_invocation_input_value_for(job_template, input) do |input_fields| %>
2
2
  <% unless input.options.blank? %>
3
- <%= selectable_f input_fields, :value, input.options_array, {:include_blank => !input.required}, :require => input.required, :label => input.name, :help_inline => input.description, :id => input.name, :onchange => "regenerate_description(this);" %>
3
+ <%= selectable_f input_fields, :value, input.options_array, { :include_blank => !input.required }, :label_help => input.description, :require => input.required, :label => input.name, :id => input.name, :onchange => "regenerate_description(this);" %>
4
4
  <% else %>
5
- <%= textarea_f input_fields, :value, :label => input.name, :help_inline => input.description, :required => input.required, :rows => 2, :onchange => "regenerate_description(this);", :id => input.name %>
5
+ <%= textarea_f input_fields, :value, :label => input.name, :label_help => input.description, :required => input.required, :rows => 2, :onchange => "regenerate_description(this);", :id => input.name %>
6
6
  <% end %>
7
7
  <% end %>
@@ -9,7 +9,7 @@
9
9
  link_to_function(_('Toggle STDERR'), '$("div.line.stderr").toggle()', :class => 'btn btn-default'),
10
10
  link_to_function(_('Toggle STDOUT'), '$("div.line.stdout").toggle()', :class => 'btn btn-default'),
11
11
  link_to_function(_('Toggle DEBUG'), '$("div.line.debug").toggle()', :class => 'btn btn-default')) %>
12
- <%= button_group(template_invocation_task_buttons(@template_invocation_task)) %>
12
+ <%= button_group(template_invocation_task_buttons(@template_invocation_task, @template_invocation.job_invocation)) %>
13
13
  </div>
14
14
  </div>
15
15
 
data/config/routes.rb CHANGED
@@ -24,6 +24,7 @@ Rails.application.routes.draw do
24
24
  end
25
25
  member do
26
26
  get 'rerun'
27
+ post 'cancel'
27
28
  end
28
29
  end
29
30
 
@@ -42,6 +43,10 @@ Rails.application.routes.draw do
42
43
  resources :hosts, :only => :none do
43
44
  get '/', :to => 'job_invocations#output'
44
45
  end
46
+ member do
47
+ post 'cancel'
48
+ post 'rerun'
49
+ end
45
50
  end
46
51
 
47
52
  resources :job_templates, :except => [:new, :edit] do
@@ -14,7 +14,7 @@ class ExpandAllTemplateInvocations < ActiveRecord::Migration[4.2]
14
14
  FakeTemplateInvocation.update_all 'host_id = NULL'
15
15
 
16
16
  # expand all pattern template invocations and link RunHostJob
17
- JobInvocation.joins(:targeting).where("#{Targeting.table_name}.resolved_at IS NOT NULL").includes([ :pattern_template_invocations, :targeting, :sub_tasks => :locks ]).each do |job_invocation|
17
+ JobInvocation.joins(:targeting).where("#{Targeting.table_name}.resolved_at IS NOT NULL").includes([:pattern_template_invocations, :targeting]).each do |job_invocation|
18
18
  job_invocation.pattern_template_invocations.each do |pattern_template_invocation|
19
19
  job_invocation.targeting.hosts.each do |host|
20
20
  task = job_invocation.sub_tasks.find do |sub_task|
@@ -0,0 +1,6 @@
1
+ class AddSecretsToJobInvocations < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :job_invocations, :password, :string
4
+ add_column :job_invocations, :key_passphrase, :string
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddNotificationBuilderToRemoteExecutionFeature < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :remote_execution_features, :notification_builder, :string
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddFeatureIdToJobInvocation < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :job_invocations, :remote_execution_feature_id, :integer, :index => true
4
+ add_foreign_key :job_invocations, :remote_execution_features, :column => :remote_execution_feature_id
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ class ChangeTaskIdToUuid < ActiveRecord::Migration[4.2]
2
+ def up
3
+ if on_postgresql?
4
+ change_table :job_invocations do |t|
5
+ t.change :task_id, :uuid, :using => 'task_id::uuid'
6
+ end
7
+
8
+ change_table :template_invocations do |t|
9
+ t.change :run_host_job_task_id, :uuid, :using => 'run_host_job_task_id::uuid'
10
+ end
11
+ end
12
+ end
13
+
14
+ def down
15
+ if on_postgresql?
16
+ change_table :job_invocations do |t|
17
+ t.change :task_id, :string, :limit => 255
18
+ end
19
+
20
+ change_table :template_invocations do |t|
21
+ t.change :run_host_job_task_id, :string, :limit => 255
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def on_postgresql?
29
+ ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
30
+ end
31
+ end
@@ -26,7 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency 'deface'
27
27
  s.add_dependency 'dynflow', '~> 0.8.26'
28
28
  s.add_dependency 'foreman_remote_execution_core'
29
- s.add_dependency 'foreman-tasks', '>= 0.9.5'
29
+ s.add_dependency 'foreman-tasks', '~> 0.12'
30
30
 
31
31
  s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
32
32
  s.add_development_dependency 'rubocop'
@@ -53,10 +53,11 @@ module ForemanRemoteExecution
53
53
  :'api/v2/job_templates' => [:destroy] }, :resource_type => 'JobTemplate'
54
54
  permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
55
55
  permission :create_job_invocations, { :job_invocations => [:new, :create, :refresh, :rerun, :preview_hosts],
56
- 'api/v2/job_invocations' => [:create] }, :resource_type => 'JobInvocation'
56
+ 'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
57
57
  permission :view_job_invocations, { :job_invocations => [:index, :show, :auto_complete_search], :template_invocations => [:show],
58
58
  'api/v2/job_invocations' => [:index, :show, :output] }, :resource_type => 'JobInvocation'
59
59
  permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
60
+ permission :cancel_job_invocations, { :job_invocations => [:cancel], 'api/v2/job_invocations' => [:cancel] }, :resource_type => 'JobInvocation'
60
61
  # this permissions grants user to get auto completion hints when setting up filters
61
62
  permission :filter_autocompletion_for_template_invocation, { :template_invocations => [ :auto_complete_search, :index ] },
62
63
  :resource_type => 'TemplateInvocation'
@@ -71,6 +72,7 @@ module ForemanRemoteExecution
71
72
  :view_smart_proxies
72
73
  ].freeze
73
74
  MANAGER_PERMISSIONS = USER_PERMISSIONS + [
75
+ :cancel_job_invocations,
74
76
  :destroy_job_templates,
75
77
  :edit_job_templates,
76
78
  :create_job_templates,
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '1.4.5'.freeze
2
+ VERSION = '1.4.6'.freeze
3
3
  end
@@ -45,7 +45,13 @@ FactoryBot.define do
45
45
  after(:build) do |invocation, evaluator|
46
46
  invocation.pattern_template_invocations << FactoryBot.build(:template_invocation)
47
47
  end
48
+ end
48
49
 
50
+ trait :with_failed_task do
51
+ after(:build) do |invocation, _evaluator|
52
+ invocation.template_invocations << FactoryBot.build(:template_invocation, :with_failed_task, :with_host)
53
+ invocation.task = FactoryBot.build(:some_task)
54
+ end
49
55
  end
50
56
 
51
57
  trait :with_task do
@@ -23,45 +23,77 @@ module Api
23
23
  assert_equal template['job_category'], @invocation.job_category
24
24
  end
25
25
 
26
- test 'should create valid with job_template_id' do
27
- attrs = { :job_category => @template.job_category, :name => 'RandomName', :job_template_id => @template.id,
28
- :targeting_type => 'static_query', :search_query => 'foobar'}
29
- post :create, params: { :job_invocation => attrs }
26
+ context 'creation' do
27
+ setup do
28
+ @attrs = { :job_category => @template.job_category,
29
+ :name => 'RandomName',
30
+ :job_template_id => @template.id,
31
+ :targeting_type => 'static_query',
32
+ :search_query => 'foobar' }
33
+ end
30
34
 
31
- invocation = ActiveSupport::JSON.decode(@response.body)
32
- assert_equal attrs[:job_category], invocation['job_category']
33
- assert_response :success
34
- end
35
+ test 'should create valid with job_template_id' do
36
+ post :create, params: { job_invocation: @attrs }
35
37
 
36
- test 'should create with description format overridden' do
37
- attrs = { :job_category => @template.job_category, :name => 'RandomName', :job_template_id => @template.id,
38
- :targeting_type => 'static_query', :search_query => 'foobar', :description_format => 'format' }
39
- post :create, params: { :job_invocation => attrs }
38
+ invocation = ActiveSupport::JSON.decode(@response.body)
39
+ assert_equal @attrs[:job_category], invocation['job_category']
40
+ assert_response :success
41
+ end
40
42
 
41
- invocation = ActiveSupport::JSON.decode(@response.body)
42
- assert_equal attrs[:description_format], invocation['description']
43
- end
43
+ test 'should create with description format overridden' do
44
+ @attrs[:description_format] = 'format'
45
+ post :create, params: { job_invocation: @attrs }
44
46
 
45
- test 'should create with recurrence' do
46
- attrs = { :job_category => @template.job_category, :name => 'RandomName',
47
- :job_template_id => @template.id,:targeting_type => 'static_query',
48
- :search_query => 'foobar', :recurrence => {:cron_line => '5 * * * *'}}
47
+ invocation = ActiveSupport::JSON.decode(@response.body)
48
+ assert_equal @attrs[:description_format], invocation['description']
49
+ end
49
50
 
50
- post :create, params: { :job_invocation => attrs }
51
- invocation = ActiveSupport::JSON.decode(@response.body)
52
- assert_equal invocation['mode'], 'recurring'
53
- assert_response :success
54
- end
51
+ test 'should create with recurrence' do
52
+ @attrs[:recurrence] = { cron_line: '5 * * * *' }
53
+ post :create, params: { job_invocation: @attrs }
54
+ invocation = ActiveSupport::JSON.decode(@response.body)
55
+ assert_equal invocation['mode'], 'recurring'
56
+ assert_response :success
57
+ end
55
58
 
56
- test 'should create with schedule' do
57
- attrs = { :job_category => @template.job_category, :name => 'RandomName',
58
- :job_template_id => @template.id,:targeting_type => 'static_query',
59
- :search_query => 'foobar', :scheduling => {:start_at => Time.now.to_s}}
59
+ test 'should create with schedule' do
60
+ @attrs[:scheduling] = { start_at: Time.now.to_s }
61
+ post :create, params: { job_invocation: @attrs }
62
+ invocation = ActiveSupport::JSON.decode(@response.body)
63
+ assert_equal invocation['mode'], 'future'
64
+ assert_response :success
65
+ end
60
66
 
61
- post :create, params: { :job_invocation => attrs }
62
- invocation = ActiveSupport::JSON.decode(@response.body)
63
- assert_equal invocation['mode'], 'future'
64
- assert_response :success
67
+ context 'with_feature' do
68
+ setup do
69
+ @feature = FactoryBot.create(:remote_execution_feature,
70
+ :job_template => @template)
71
+ @attrs = {
72
+ feature: @feature.label
73
+ }
74
+ end
75
+
76
+ test 'host ids as array of FQDNs' do
77
+ host = FactoryBot.create(:host)
78
+ @attrs[:host_ids] = [host.fqdn]
79
+ post :create, params: { job_invocation: @attrs }
80
+ assert_response :success
81
+ end
82
+
83
+ test 'host ids as array of IDs' do
84
+ host = FactoryBot.create(:host)
85
+ host2 = FactoryBot.create(:host)
86
+ @attrs[:host_ids] = [host.id, host2.id]
87
+ post :create, params: { job_invocation: @attrs }
88
+ assert_response :success
89
+ end
90
+
91
+ test 'search_query' do
92
+ @attrs[:host_ids] = 'name = testfqdn'
93
+ post :create, params: { job_invocation: @attrs }
94
+ assert_response :success
95
+ end
96
+ end
65
97
  end
66
98
 
67
99
  test 'should provide output for delayed task' do
@@ -74,6 +106,81 @@ module Api
74
106
  assert_equal result['output'], []
75
107
  assert_response :success
76
108
  end
109
+
110
+ test 'should cancel a job' do
111
+ @invocation.task.expects(:cancellable?).returns(true)
112
+ @invocation.task.expects(:cancel).returns(true)
113
+ JobInvocation.expects(:from_param).with(@invocation.id.to_s).returns(@invocation)
114
+ post :cancel, :params => { :id => @invocation.id }
115
+ result = ActiveSupport::JSON.decode(@response.body)
116
+ assert_equal result['cancelled'], true
117
+ assert_equal result['id'], @invocation.id
118
+
119
+ assert_response :success
120
+ end
121
+
122
+ test 'should abort a job' do
123
+ @invocation.task.expects(:cancellable?).returns(true)
124
+ @invocation.task.expects(:abort).returns(true)
125
+ JobInvocation.expects(:from_param).with(@invocation.id.to_s).returns(@invocation)
126
+ post :cancel, :params => { :id => @invocation.id, :force => true }
127
+ result = ActiveSupport::JSON.decode(@response.body)
128
+ assert_equal result['cancelled'], true
129
+ assert_equal result['id'], @invocation.id
130
+
131
+ assert_response :success
132
+ end
133
+
134
+ test 'should error when trying to cancel a stopped job' do
135
+ @invocation.task.expects(:cancellable?).returns(false)
136
+ JobInvocation.expects(:from_param).with(@invocation.id.to_s).returns(@invocation)
137
+ post :cancel, :params => { :id => @invocation.id }
138
+ assert_response 422
139
+ end
140
+
141
+ test 'should rerun' do
142
+ JobInvocation.any_instance.expects(:generate_description)
143
+ JobInvocationComposer.any_instance
144
+ .expects(:validate_job_category)
145
+ .with(@invocation.job_category)
146
+ .returns(@invocation.job_category)
147
+ post :rerun, params: { :id => @invocation.id }
148
+ assert_response :success
149
+ result = ActiveSupport::JSON.decode(@response.body)
150
+ targeting = Targeting.find(result['targeting_id'])
151
+ targeting.user_id.must_equal users(:admin).id
152
+ targeting.search_query.must_equal @invocation.targeting.search_query
153
+ end
154
+
155
+ test 'should not raise an exception when reruning failed has no hosts' do
156
+ JobInvocation.any_instance.expects(:generate_description)
157
+ JobInvocationComposer.any_instance
158
+ .expects(:validate_job_category)
159
+ .with(@invocation.job_category)
160
+ .returns(@invocation.job_category)
161
+ post :rerun, params: { :id => @invocation.id, :failed_only => true }
162
+ assert_response :success
163
+ result = ActiveSupport::JSON.decode(@response.body)
164
+ targeting = Targeting.find(result['targeting_id'])
165
+ targeting.user_id.must_equal users(:admin).id
166
+ targeting.search_query.must_equal 'name ^ ()'
167
+ end
168
+
169
+ test 'should rerun failed only' do
170
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_failed_task)
171
+ JobInvocation.any_instance.expects(:generate_description)
172
+ JobInvocationComposer.any_instance
173
+ .expects(:validate_job_category)
174
+ .with(@invocation.job_category)
175
+ .returns(@invocation.job_category)
176
+ post :rerun, params: { :id => @invocation.id, :failed_only => true }
177
+ assert_response :success
178
+ result = ActiveSupport::JSON.decode(@response.body)
179
+ targeting = Targeting.find(result['targeting_id'])
180
+ hostnames = @invocation.template_invocations.map { |ti| ti.host.name }
181
+ targeting.user_id.must_equal users(:admin).id
182
+ targeting.search_query.must_equal "name ^ (#{hostnames.join(',')})"
183
+ end
77
184
  end
78
185
  end
79
186
  end