foreman_remote_execution 4.5.5 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
  5. data/app/graphql/types/job_invocation.rb +16 -0
  6. data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  8. data/app/mailers/rex_job_mailer.rb +15 -0
  9. data/app/models/job_invocation.rb +4 -0
  10. data/app/models/job_invocation_composer.rb +21 -13
  11. data/app/models/job_template.rb +1 -1
  12. data/app/models/remote_execution_provider.rb +17 -2
  13. data/app/models/rex_mail_notification.rb +13 -0
  14. data/app/models/setting/remote_execution.rb +7 -1
  15. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  16. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  17. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  18. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  19. data/app/views/template_invocations/show.html.erb +2 -1
  20. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  21. data/db/seeds.d/95-mail_notifications.rb +24 -0
  22. data/foreman_remote_execution.gemspec +2 -4
  23. data/lib/foreman_remote_execution/engine.rb +4 -0
  24. data/lib/foreman_remote_execution/version.rb +1 -1
  25. data/package.json +6 -6
  26. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  27. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  28. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  29. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  30. data/test/unit/concerns/host_extensions_test.rb +4 -4
  31. data/test/unit/input_template_renderer_test.rb +1 -89
  32. data/test/unit/job_invocation_composer_test.rb +1 -12
  33. data/test/unit/job_invocation_report_template_test.rb +15 -12
  34. data/test/unit/remote_execution_provider_test.rb +34 -0
  35. data/webpack/JobWizard/JobWizard.js +53 -20
  36. data/webpack/JobWizard/JobWizard.scss +33 -4
  37. data/webpack/JobWizard/JobWizardConstants.js +17 -0
  38. data/webpack/JobWizard/__tests__/fixtures.js +8 -0
  39. data/webpack/JobWizard/__tests__/integration.test.js +3 -7
  40. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
  41. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  45. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
  46. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  47. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
  50. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  51. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
  52. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
  53. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
  54. data/webpack/JobWizard/steps/Schedule/index.js +89 -28
  55. data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
  56. data/webpack/JobWizard/steps/form/Formatter.js +10 -9
  57. data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
  58. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  59. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  60. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  61. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  62. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  63. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  64. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  65. metadata +26 -19
  66. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99202dff05c36d40eac36950a23298ca1cf2ca4510e2506787bace85c72f0fd9
4
- data.tar.gz: '059cd0dd0b427bb2fc1a80c59a47ec26e2fedb4d0e27f989c1ec859e52e1191a'
3
+ metadata.gz: c7565defbf0881174acb2a48e274aacb3d790d2ee8b9de3119307c2afabd7355
4
+ data.tar.gz: acffdb2df04d0c1caeff65513e1a63e5cf66276897206affd606c5136b7afba8
5
5
  SHA512:
6
- metadata.gz: 19e978232f3ee68a30d65ceb7c1b799b180894c16f5060c80ed4c5b85870fbe1f0f7ed320b44924cbb7177e280deb32309a061ab2737ff9bc2cc944092a48b63
7
- data.tar.gz: 9ac86095eb5c6cdf2bed666e911895462e44b0cbf96ddf40023c062604f0de947ebf4851e48d556332ed6624a59d6748608bdb472ce63c3e593ebcc3e5a21d52
6
+ metadata.gz: 7064c4e3984fd4ef3ef463fc10cb27e911d1f24ac0b74ccf5fb3b7682ba3d4c3418e130f5b6e0f1920ed83406dbf0787585ef1e1b83cfd0c0e412219c059309c
7
+ data.tar.gz: ab22827c2750d67893621267a43095c2b845937b6d72d5a665bf800ab97542a06cda1480187174943077c9365caac0076d67f765b6c43c4e01650dca1f7b9010
@@ -71,3 +71,10 @@ jobs:
71
71
  run: |
72
72
  bundle exec rake test:foreman_remote_execution
73
73
  bundle exec rake test TEST="test/unit/foreman/access_permissions_test.rb"
74
+ - name: 'Upload logs'
75
+ uses: actions/upload-artifact@v2
76
+ if: failure()
77
+ with:
78
+ name: logs
79
+ path: log/*.log
80
+ retention-days: 5
data/.rubocop_todo.yml CHANGED
@@ -220,6 +220,7 @@ Naming/FileName:
220
220
  - 'db/seeds.d/60-ssh_proxy_feature.rb'
221
221
  - 'db/seeds.d/70-job_templates.rb'
222
222
  - 'db/seeds.d/90-bookmarks.rb'
223
+ - 'db/seeds.d/95-mail_notifications.rb'
223
224
 
224
225
  # Offense count: 1
225
226
  # Configuration parameters: ForbiddenDelimiters.
@@ -41,12 +41,18 @@ module Api
41
41
  param :effective_user, String,
42
42
  :required => false,
43
43
  :desc => N_('What user should be used to run the script (using sudo-like mechanisms). Defaults to a template parameter or global setting.')
44
+ param :effective_user_password, String,
45
+ :required => false,
46
+ :desc => N_('Set password for effective user (using sudo-like mechanisms)')
44
47
  end
48
+ param :password, String, :required => false, :desc => N_('Set SSH password')
49
+ param :key_passphrase, String, :required => false, :desc => N_('Set SSH key passphrase')
45
50
 
46
51
  param :recurrence, Hash, :desc => N_('Create a recurring job') do
47
52
  param :cron_line, String, :required => false, :desc => N_('How often the job should occur, in the cron format')
48
53
  param :max_iteration, :number, :required => false, :desc => N_('Repeat a maximum of N times')
49
54
  param :end_time, DateTime, :required => false, :desc => N_('Perform no more executions after this time')
55
+ param :purpose, String, :required => false, :desc => N_('Designation of a special purpose')
50
56
  end
51
57
 
52
58
  param :scheduling, Hash, :desc => N_('Schedule the job to start at a later time') do
@@ -197,7 +203,7 @@ module Api
197
203
  end
198
204
 
199
205
  if job_invocation_params.key?(:ssh)
200
- job_invocation_params.merge!(job_invocation_params.delete(:ssh).permit(:effective_user))
206
+ job_invocation_params.merge!(job_invocation_params.delete(:ssh).permit(:effective_user, :effective_user_password))
201
207
  end
202
208
 
203
209
  job_invocation_params[:inputs] ||= {}
@@ -0,0 +1,16 @@
1
+ module Types
2
+ class JobInvocation < BaseObject
3
+ description 'A Job Invocation'
4
+
5
+ global_id_field :id
6
+ field :job_category, String
7
+ field :description, String
8
+ field :time_span, Integer
9
+ field :start_at, GraphQL::Types::ISO8601DateTime
10
+ field :status_label, String
11
+
12
+ belongs_to :triggering, Types::Triggering
13
+ belongs_to :task, Types::Task
14
+ field :recurring_logic, Types::RecurringLogic
15
+ end
16
+ end
@@ -56,7 +56,8 @@ module Actions
56
56
  :secrets => secrets(host, job_invocation, provider),
57
57
  :use_batch_triggering => true,
58
58
  :use_concurrency_control => options[:use_concurrency_control],
59
- :first_execution => first_execution }
59
+ :first_execution => first_execution,
60
+ :alternative_names => provider.alternative_names(host) }
60
61
  action_options = provider.proxy_command_options(template_invocation, host)
61
62
  .merge(additional_options)
62
63
 
@@ -9,7 +9,9 @@ module Actions
9
9
  middleware.use Actions::Middleware::BindJobInvocation
10
10
  middleware.use Actions::Middleware::RecurringLogic
11
11
  middleware.use Actions::Middleware::WatchDelegatedProxySubTasks
12
- middleware.use Actions::Middleware::ProxyBatchTriggering
12
+
13
+ execution_plan_hooks.use :notify_on_success, :on => :success
14
+ execution_plan_hooks.use :notify_on_failure, :on => :failure
13
15
 
14
16
  class CheckOnProxyActions; end
15
17
 
@@ -32,6 +34,10 @@ module Actions
32
34
  set_up_concurrency_control job_invocation
33
35
  input.update(:job_category => job_invocation.job_category)
34
36
  plan_self(:job_invocation_id => job_invocation.id)
37
+ provider = job_invocation.pattern_template_invocations.first&.template&.provider
38
+ input[:proxy_batch_size] ||= provider&.proxy_batch_size || Setting['foreman_tasks_proxy_batch_size']
39
+ trigger_action = plan_action(Actions::TriggerProxyBatch, batch_size: proxy_batch_size, total_count: hosts.count)
40
+ input[:trigger_run_step_id] = trigger_action.run_step_id
35
41
  end
36
42
 
37
43
  def create_sub_plans
@@ -46,14 +52,47 @@ module Actions
46
52
  end
47
53
  end
48
54
 
55
+ def spawn_plans
56
+ super
57
+ ensure
58
+ trigger_remote_batch
59
+ end
60
+
61
+ def trigger_remote_batch
62
+ batches_ready = (output[:planned_count] - output[:remote_triggered_count]) / proxy_batch_size
63
+ return unless batches_ready > 0
64
+
65
+ plan_event(Actions::TriggerProxyBatch::TriggerNextBatch[batches_ready], nil, step_id: input[:trigger_run_step_id])
66
+ output[:remote_triggered_count] += proxy_batch_size * batches_ready
67
+ end
68
+
69
+ def on_planning_finished
70
+ plan_event(Actions::TriggerProxyBatch::TriggerLastBatch, nil, step_id: input[:trigger_run_step_id])
71
+ super
72
+ end
73
+
49
74
  def finalize
50
75
  job_invocation.password = job_invocation.key_passphrase = job_invocation.effective_user_password = nil
51
76
  job_invocation.save!
52
77
 
53
78
  Rails.logger.debug "cleaning cache for keys that begin with 'job_invocation_#{job_invocation.id}'"
54
79
  Rails.cache.delete_matched(/\A#{JobInvocation::CACHE_PREFIX}_#{job_invocation.id}/)
55
- # creating the success notification should be the very last thing this tasks do
80
+ end
81
+
82
+ def notify_on_success(_plan)
56
83
  job_invocation.build_notification.deliver!
84
+
85
+ if [RexMailNotification::SUCCEEDED_JOBS, RexMailNotification::ALL_JOBS].include?(mail_notification_preference&.interval)
86
+ RexJobMailer.job_finished(job_invocation, subject: _("REX job has succeeded - %s") % job_invocation.to_s).deliver_now
87
+ end
88
+ end
89
+
90
+ def notify_on_failure(_plan)
91
+ job_invocation.build_notification.deliver!
92
+
93
+ if [RexMailNotification::FAILED_JOBS, RexMailNotification::ALL_JOBS].include?(mail_notification_preference&.interval)
94
+ RexJobMailer.job_finished(job_invocation, subject: _("REX job has failed - %s") % job_invocation.to_s).deliver_now
95
+ end
57
96
  end
58
97
 
59
98
  def job_invocation
@@ -67,6 +106,7 @@ module Actions
67
106
 
68
107
  def initiate
69
108
  output[:host_count] = total_count
109
+ output[:remote_triggered_count] = 0
70
110
  super
71
111
  end
72
112
 
@@ -94,7 +134,11 @@ module Actions
94
134
  end
95
135
 
96
136
  def run(event = nil)
97
- super unless event == Dynflow::Action::Skip
137
+ if event == Dynflow::Action::Skip
138
+ plan_event(Dynflow::Action::Skip, nil, step_id: input[:trigger_run_step_id])
139
+ else
140
+ super
141
+ end
98
142
  end
99
143
 
100
144
  def humanized_input
@@ -104,6 +148,16 @@ module Actions
104
148
  def humanized_name
105
149
  '%s:' % _(super)
106
150
  end
151
+
152
+ def proxy_batch_size
153
+ input[:proxy_batch_size]
154
+ end
155
+
156
+ private
157
+
158
+ def mail_notification_preference
159
+ UserMailNotification.where(mail_notification_id: RexMailNotification.first, user_id: User.current.id).first
160
+ end
107
161
  end
108
162
  end
109
163
  end
@@ -0,0 +1,15 @@
1
+ class RexJobMailer < ApplicationMailer
2
+ add_template_helper(ApplicationHelper)
3
+
4
+ def job_finished(job, opts = {})
5
+ @job = job
6
+ @subject = opts[:subject] || _('REX job has finished - %s') % @job.to_s
7
+
8
+ if @job.user.nil?
9
+ Rails.logger.warn 'Job user no longer exist, skipping email notification'
10
+ return
11
+ end
12
+
13
+ mail(to: @job.user.mail, subject: @subject)
14
+ end
15
+ end
@@ -65,6 +65,8 @@ class JobInvocation < ApplicationRecord
65
65
  scoped_search :on => 'pattern_template_name', :rename => 'pattern_template_name', :operators => ['= '],
66
66
  :complete_value => false, :only_explicit => true, :ext_method => :search_by_pattern_template
67
67
 
68
+ scoped_search :relation => :recurring_logic, :on => 'purpose', :rename => 'recurring_logic.purpose'
69
+
68
70
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id'
69
71
 
70
72
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring',
@@ -73,6 +75,8 @@ class JobInvocation < ApplicationRecord
73
75
 
74
76
  default_scope -> { order('job_invocations.id DESC') }
75
77
 
78
+ scope :latest_jobs, -> { unscoped.joins(:task).order('foreman_tasks_tasks.start_at DESC').authorized(:view_job_invocations).limit(5) }
79
+
76
80
  validates_lengths_from_database :only => [:description]
77
81
 
78
82
  attr_accessor :start_before, :description_format
@@ -121,7 +121,10 @@ class JobInvocationComposer
121
121
  :targeting => targeting_params,
122
122
  :triggering => triggering_params,
123
123
  :description_format => api_params[:description_format],
124
+ :password => api_params[:password],
124
125
  :remote_execution_feature_id => remote_execution_feature_id,
126
+ :effective_user_password => api_params[:effective_user_password],
127
+ :key_passphrase => api_params[:key_passphrase],
125
128
  :concurrency_control => concurrency_control_params,
126
129
  :execution_timeout_interval => api_params[:execution_timeout_interval] || template.execution_timeout_interval,
127
130
  :template_invocations => template_invocations_params }.with_indifferent_access
@@ -138,17 +141,11 @@ class JobInvocationComposer
138
141
  end
139
142
 
140
143
  def triggering_params
141
- raise ::Foreman::Exception, _('Cannot specify both recurrence and scheduling') if api_params[:recurrence].present? && api_params[:scheduling].present?
142
-
143
- if api_params[:recurrence].present?
144
- {
145
- :mode => :recurring,
146
- :cronline => api_params[:recurrence][:cron_line],
147
- :end_time => format_datetime(api_params[:recurrence][:end_time]),
148
- :input_type => :cronline,
149
- :max_iteration => api_params[:recurrence][:max_iteration],
150
- }
151
- elsif api_params[:scheduling].present?
144
+ if api_params[:recurrence].present? && api_params[:scheduling].present?
145
+ recurring_mode_params.merge :start_at_raw => format_datetime(api_params[:scheduling][:start_at])
146
+ elsif api_params[:recurrence].present? && api_params[:scheduling].empty?
147
+ recurring_mode_params
148
+ elsif api_params[:recurrence].empty? && api_params[:scheduling].present?
152
149
  {
153
150
  :mode => :future,
154
151
  :start_at_raw => format_datetime(api_params[:scheduling][:start_at]),
@@ -160,6 +157,17 @@ class JobInvocationComposer
160
157
  end
161
158
  end
162
159
 
160
+ def recurring_mode_params
161
+ {
162
+ :mode => :recurring,
163
+ :cronline => api_params[:recurrence][:cron_line],
164
+ :end_time => format_datetime(api_params[:recurrence][:end_time]),
165
+ :input_type => :cronline,
166
+ :max_iteration => api_params[:recurrence][:max_iteration],
167
+ :purpose => api_params[:recurrence][:purpose],
168
+ }
169
+ end
170
+
163
171
  def concurrency_control_params
164
172
  {
165
173
  :level => api_params.fetch(:concurrency_control, {})[:concurrency_level],
@@ -513,7 +521,7 @@ class JobInvocationComposer
513
521
  end
514
522
 
515
523
  def available_bookmarks
516
- Bookmark.authorized(:view_bookmarks).my_bookmarks.where(:controller => ['hosts', 'dashboard'])
524
+ Bookmark.my_bookmarks.where(:controller => ['hosts', 'dashboard'])
517
525
  end
518
526
 
519
527
  def targeted_hosts
@@ -636,7 +644,7 @@ class JobInvocationComposer
636
644
  setting_value = Setting['remote_execution_form_job_template']
637
645
  return default_value unless setting_value
638
646
 
639
- form_template = JobTemplate.find_by :name => setting_value
647
+ form_template = JobTemplate.authorized(:view_job_templates).find_by :name => setting_value
640
648
  return default_value unless form_template
641
649
 
642
650
  if block_given?
@@ -83,7 +83,7 @@ class JobTemplate < ::Template
83
83
  end
84
84
 
85
85
  def acceptable_template_input_types
86
- [ :user, :fact, :variable, :puppet_parameter ]
86
+ [ :user, :fact, :variable ]
87
87
  end
88
88
 
89
89
  def default_render_scope_class
@@ -80,7 +80,14 @@ class RemoteExecutionProvider
80
80
 
81
81
  def find_ip(host, interfaces)
82
82
  if host_setting(host, :remote_execution_connect_by_ip)
83
- interfaces.find { |i| i.ip.present? }.try(:ip)
83
+ ip4_address = interfaces.find { |i| i.ip.present? }.try(:ip)
84
+ ip6_address = interfaces.find { |i| i.ip6.present? }.try(:ip6)
85
+
86
+ if host_setting(host, :remote_execution_connect_by_ip_prefer_ipv6)
87
+ ip6_address || ip4_address
88
+ else
89
+ ip4_address || ip6_address
90
+ end
84
91
  end
85
92
  end
86
93
 
@@ -112,7 +119,11 @@ class RemoteExecutionProvider
112
119
  end
113
120
 
114
121
  def proxy_action_class
115
- 'ForemanRemoteExecutionCore::Actions::RunScript'
122
+ 'Proxy::RemoteExecution::Ssh::Actions::RunScript'
123
+ end
124
+
125
+ def proxy_batch_size
126
+ Setting['foreman_tasks_proxy_batch_size']
116
127
  end
117
128
 
118
129
  # Return a specific proxy selector to use for running a given template
@@ -124,5 +135,9 @@ class RemoteExecutionProvider
124
135
  ::DefaultProxyProxySelector.new
125
136
  end
126
137
  end
138
+
139
+ def alternative_names(host)
140
+ { :fqdn => find_fqdn(effective_interfaces(host)) }
141
+ end
127
142
  end
128
143
  end
@@ -0,0 +1,13 @@
1
+ class RexMailNotification < MailNotification
2
+ FAILED_JOBS = N_("Subscribe to my failed jobs")
3
+ SUCCEEDED_JOBS = N_("Subscribe to my succeeded jobs")
4
+ ALL_JOBS = N_("Subscribe to all my jobs")
5
+
6
+ def subscription_options
7
+ [
8
+ FAILED_JOBS,
9
+ SUCCEEDED_JOBS,
10
+ ALL_JOBS,
11
+ ]
12
+ end
13
+ end
@@ -39,9 +39,15 @@ class Setting::RemoteExecution < Setting
39
39
  self.set('remote_execution_connect_by_ip',
40
40
  N_('Should the ip addresses on host interfaces be preferred over the fqdn? '\
41
41
  '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. '\
42
- 'This setting only applies to IPv4. When the host has only an IPv6 address on the interface used for remote execution, hostname will be used even if this setting is set to true.'),
42
+ 'For dual-stacked hosts you should consider the remote_execution_connect_by_ip_prefer_ipv6 setting'),
43
43
  false,
44
44
  N_('Connect by IP')),
45
+ self.set('remote_execution_connect_by_ip_prefer_ipv6',
46
+ N_('When connecting using ip address, should the IPv6 addresses be preferred? '\
47
+ 'If no IPv6 address is set, it falls back to IPv4 automatically. You may override this per host by setting a parameter called remote_execution_connect_by_ip_prefer_ipv6. '\
48
+ 'By default and for compatibility, IPv4 will be preferred over IPv6 by default'),
49
+ false,
50
+ N_('Prefer IPv6 over IPv4')),
45
51
  self.set('remote_execution_ssh_password',
46
52
  N_('Default password to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_password'),
47
53
  nil,
@@ -20,7 +20,8 @@ module UINotifications
20
20
  end
21
21
 
22
22
  def blueprint
23
- @blueprint ||= NotificationBlueprint.unscoped.find_by(:name => 'rex_job_succeeded')
23
+ blueprint = @subject.status == HostStatus::ExecutionStatus::ERROR ? 'rex_job_failed' : 'rex_job_succeeded'
24
+ @blueprint ||= NotificationBlueprint.unscoped.find_by(:name => blueprint)
24
25
  end
25
26
 
26
27
  def message
@@ -0,0 +1,21 @@
1
+ <h4 class="header">
2
+ <%= link_to _("Latest Jobs"), job_invocations_path(:order=>'start_at DESC') %>
3
+ </h4>
4
+ <% if JobInvocation.latest_jobs.any? %>
5
+ <table class="<%= table_css_classes('table-fixed') %>">
6
+ <tr>
7
+ <th class="col-md-5"><%= _("Name") %></th>
8
+ <th class="col-md-2"><%= _("State") %></th>
9
+ <th class="col-md-3"><%= _("Started") %></th>
10
+ </tr>
11
+ <% JobInvocation.latest_jobs.each do |invocation| %>
12
+ <tr>
13
+ <td class="ellipsis"><%= link_to_if_authorized invocation_description(invocation), hash_for_job_invocation_path(invocation).merge(:auth_object => invocation, :permission => :view_job_invocations, :authorizer => authorizer) %></td>
14
+ <td><%= link_to_invocation_task_if_authorized(invocation) %></td>
15
+ <td><%= time_in_words_span(invocation.start_at) %></td>
16
+ </tr>
17
+ <% end %>
18
+ </table>
19
+ <% else %>
20
+ <p class="ca"><%= _("No jobs available") %></p>
21
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <p>
2
+ <%= _("A job '%{job_name}' has %{status} at %{time}") % {
3
+ :job_name => @job.to_s, :status => @job.status_label, :time => date_time_absolute_value(@job.task.ended_at)
4
+ } %>
5
+ </p>
6
+
7
+ <div class="dashboard">
8
+ <table>
9
+ <tr>
10
+ <td width="18%" class="hosts-rows"><b><%= _("Job result") %></b></td>
11
+ <td width="82%" class="hosts-rows"><%= @job.status_label %></td>
12
+ </tr>
13
+ <tr>
14
+ <td width="18%" class="hosts-rows"><b><%= _("Total hosts") %></b></td>
15
+ <td width="82%" class="hosts-rows"><%= @job.total_hosts_count %></td>
16
+ </tr>
17
+ <tr>
18
+ <td width="18%" class="hosts-rows"><b><%= _("Failed hosts") %></b></td>
19
+ <td width="82%" class="hosts-rows"><%= @job.failed_hosts.count %></td>
20
+ </tr>
21
+ </table>
22
+ </div>
23
+
24
+ <%= link_to 'More details', job_invocation_url(@job) %>
@@ -0,0 +1,9 @@
1
+ <%= _("A job '%{job_name}' has %{status} at %{time}") % {
2
+ :job_name => @job.to_s, :status => @job.status_label, :time => date_time_absolute_value(@job.task.ended_at)
3
+ } %>
4
+
5
+ <%= _('Job result') %>: <%= @job.status_label %>
6
+ <%= _('Total hosts') %>: <%= @job.total_hosts_count %>
7
+ <%= _('Failed hosts') %>: <%= @job.failed_hosts.count %>
8
+
9
+ <%= _('See more details at %s') % job_invocation_url(@job) %>